UNPKG

ai-functions

Version:

Core AI primitives for building intelligent applications

761 lines (630 loc) 22.1 kB
/** * Tests for DigitalObjectsFunctionRegistry * * This tests the persistent function registry implementation using digital-objects. */ import { describe, it, expect, beforeEach } from 'vitest' import { createMemoryProvider } from 'digital-objects' import type { DigitalObjectsProvider } from 'digital-objects' import { createDigitalObjectsRegistry, DigitalObjectsFunctionRegistry, FUNCTION_NOUNS, FUNCTION_VERBS, } from '../src/digital-objects-registry.js' import { defineFunction } from '../src/index.js' import type { DefinedFunction } from '../src/types.js' describe('createDigitalObjectsRegistry', () => { let provider: DigitalObjectsProvider beforeEach(() => { provider = createMemoryProvider() }) it('creates a registry with nouns defined', async () => { const registry = await createDigitalObjectsRegistry({ provider }) // Check that nouns were defined const nouns = await provider.listNouns() const nounNames = nouns.map((n) => n.name) expect(nounNames).toContain(FUNCTION_NOUNS.CODE) expect(nounNames).toContain(FUNCTION_NOUNS.GENERATIVE) expect(nounNames).toContain(FUNCTION_NOUNS.AGENTIC) expect(nounNames).toContain(FUNCTION_NOUNS.HUMAN) }) it('creates a registry with verbs defined', async () => { const registry = await createDigitalObjectsRegistry({ provider }) // Check that verbs were defined const verbs = await provider.listVerbs() const verbNames = verbs.map((v) => v.name) expect(verbNames).toContain(FUNCTION_VERBS.DEFINE) expect(verbNames).toContain(FUNCTION_VERBS.CALL) expect(verbNames).toContain(FUNCTION_VERBS.COMPLETE) expect(verbNames).toContain(FUNCTION_VERBS.FAIL) }) it('can skip auto-initialization', async () => { const registry = await createDigitalObjectsRegistry({ provider, autoInitialize: false, }) // Should not have defined nouns yet const nouns = await provider.listNouns() expect(nouns.length).toBe(0) // Can manually initialize await registry.initialize() const nounsAfter = await provider.listNouns() expect(nounsAfter.length).toBe(4) }) }) describe('DigitalObjectsFunctionRegistry', () => { let provider: DigitalObjectsProvider let registry: DigitalObjectsFunctionRegistry beforeEach(async () => { provider = createMemoryProvider() registry = await createDigitalObjectsRegistry({ provider }) }) describe('set()', () => { it('stores function definitions as Things', async () => { const fn = defineFunction({ type: 'generative', name: 'summarize', args: { text: 'Text to summarize' }, output: 'string', }) registry.set('summarize', fn) // Wait for async storage to complete await new Promise((resolve) => setTimeout(resolve, 10)) // Check that it was stored as a Thing const things = await provider.find(FUNCTION_NOUNS.GENERATIVE, { name: 'summarize' }) expect(things.length).toBe(1) expect(things[0].data).toMatchObject({ name: 'summarize', type: 'generative', output: 'string', }) }) it('stores different function types in their respective nouns', async () => { const codeFn = defineFunction({ type: 'code', name: 'implement', args: { spec: 'Spec' }, language: 'typescript', }) const agenticFn = defineFunction({ type: 'agentic', name: 'research', args: { topic: 'Topic' }, instructions: 'Research thoroughly', }) const humanFn = defineFunction({ type: 'human', name: 'approve', args: { amount: 'Amount' }, channel: 'web', instructions: 'Review and approve', }) registry.set('implement', codeFn) registry.set('research', agenticFn) registry.set('approve', humanFn) await new Promise((resolve) => setTimeout(resolve, 10)) const codeThings = await provider.find(FUNCTION_NOUNS.CODE, { name: 'implement' }) const agenticThings = await provider.find(FUNCTION_NOUNS.AGENTIC, { name: 'research' }) const humanThings = await provider.find(FUNCTION_NOUNS.HUMAN, { name: 'approve' }) expect(codeThings.length).toBe(1) expect(agenticThings.length).toBe(1) expect(humanThings.length).toBe(1) }) }) describe('get()', () => { it('retrieves function definitions from cache', () => { const fn = defineFunction({ type: 'generative', name: 'translate', args: { text: 'Text', lang: 'Target language' }, output: 'string', }) registry.set('translate', fn) const retrieved = registry.get('translate') expect(retrieved).toBe(fn) expect(retrieved?.definition.name).toBe('translate') }) it('returns undefined for non-existent functions', () => { const result = registry.get('nonexistent') expect(result).toBeUndefined() }) }) describe('getAsync()', () => { it('retrieves function definitions from storage', async () => { const fn = defineFunction({ type: 'generative', name: 'analyze', args: { data: 'Data to analyze' }, output: 'string', }) await registry.setAsync('analyze', fn) // Create a new registry to simulate fresh load const newRegistry = await createDigitalObjectsRegistry({ provider }) const retrieved = await newRegistry.getAsync('analyze') expect(retrieved).toBeDefined() expect(retrieved?.definition.name).toBe('analyze') expect(retrieved?.definition.type).toBe('generative') }) }) describe('has()', () => { it('checks if function exists in cache', () => { expect(registry.has('test')).toBe(false) const fn = defineFunction({ type: 'generative', name: 'test', args: {}, output: 'string', }) registry.set('test', fn) expect(registry.has('test')).toBe(true) }) }) describe('hasAsync()', () => { it('checks if function exists in storage', async () => { const fn = defineFunction({ type: 'generative', name: 'stored', args: {}, output: 'string', }) await registry.setAsync('stored', fn) // Create new registry const newRegistry = await createDigitalObjectsRegistry({ provider }) expect(await newRegistry.hasAsync('stored')).toBe(true) expect(await newRegistry.hasAsync('notexist')).toBe(false) }) }) describe('delete()', () => { it('removes functions from cache', () => { const fn = defineFunction({ type: 'generative', name: 'toDelete', args: {}, output: 'string', }) registry.set('toDelete', fn) expect(registry.has('toDelete')).toBe(true) const result = registry.delete('toDelete') expect(result).toBe(true) expect(registry.has('toDelete')).toBe(false) }) it('returns false for non-existent functions', () => { const result = registry.delete('nonexistent') expect(result).toBe(false) }) }) describe('deleteAsync()', () => { it('removes functions from storage', async () => { const fn = defineFunction({ type: 'generative', name: 'toRemove', args: {}, output: 'string', }) await registry.setAsync('toRemove', fn) // Verify it exists const things = await provider.find(FUNCTION_NOUNS.GENERATIVE, { name: 'toRemove' }) expect(things.length).toBe(1) // Delete it const result = await registry.deleteAsync('toRemove') expect(result).toBe(true) // Verify it's gone const thingsAfter = await provider.find(FUNCTION_NOUNS.GENERATIVE, { name: 'toRemove' }) expect(thingsAfter.length).toBe(0) }) }) describe('list() and listAsync()', () => { it('list() returns all function names from cache', () => { const fn1 = defineFunction({ type: 'generative', name: 'func1', args: {}, output: 'string', }) const fn2 = defineFunction({ type: 'code', name: 'func2', args: {}, language: 'typescript', }) registry.set('func1', fn1) registry.set('func2', fn2) const names = registry.list() expect(names).toContain('func1') expect(names).toContain('func2') expect(names.length).toBe(2) }) it('listAsync() returns all function names including from storage', async () => { const fn1 = defineFunction({ type: 'generative', name: 'funcA', args: {}, output: 'string', }) const fn2 = defineFunction({ type: 'agentic', name: 'funcB', args: {}, instructions: 'Do something', }) await registry.setAsync('funcA', fn1) await registry.setAsync('funcB', fn2) const names = await registry.listAsync() expect(names).toContain('funcA') expect(names).toContain('funcB') }) }) describe('getAll() equivalent - list + get pattern', () => { it('returns all functions', () => { const fn1 = defineFunction({ type: 'generative', name: 'alpha', args: {}, output: 'string', }) const fn2 = defineFunction({ type: 'generative', name: 'beta', args: {}, output: 'string', }) const fn3 = defineFunction({ type: 'code', name: 'gamma', args: {}, language: 'python', }) registry.set('alpha', fn1) registry.set('beta', fn2) registry.set('gamma', fn3) // getAll pattern: list all names then get each const allFunctions = registry.list().map((name) => registry.get(name)) expect(allFunctions.length).toBe(3) expect(allFunctions.map((f) => f?.definition.name)).toContain('alpha') expect(allFunctions.map((f) => f?.definition.name)).toContain('beta') expect(allFunctions.map((f) => f?.definition.name)).toContain('gamma') }) }) describe('clear() and clearAsync()', () => { it('clear() removes all functions from cache', () => { const fn1 = defineFunction({ type: 'generative', name: 'a', args: {}, output: 'string', }) const fn2 = defineFunction({ type: 'generative', name: 'b', args: {}, output: 'string', }) registry.set('a', fn1) registry.set('b', fn2) expect(registry.list().length).toBe(2) registry.clear() expect(registry.list().length).toBe(0) }) it('clearAsync() removes all functions from storage', async () => { const fn = defineFunction({ type: 'generative', name: 'toClear', args: {}, output: 'string', }) await registry.setAsync('toClear', fn) // Verify stored let things = await provider.list(FUNCTION_NOUNS.GENERATIVE) expect(things.length).toBe(1) await registry.clearAsync() // Verify cleared things = await provider.list(FUNCTION_NOUNS.GENERATIVE) expect(things.length).toBe(0) }) }) }) describe('DigitalObjectsFunctionRegistry - Action Tracking', () => { let provider: DigitalObjectsProvider let registry: DigitalObjectsFunctionRegistry beforeEach(async () => { provider = createMemoryProvider() registry = await createDigitalObjectsRegistry({ provider }) }) describe('trackCall()', () => { it('creates an Action for function invocation', async () => { // First, store a function const fn = defineFunction({ type: 'generative', name: 'greet', args: { name: 'Name to greet' }, output: 'string', }) await registry.setAsync('greet', fn) // Track a call const callAction = await registry.trackCall('greet', { name: 'Alice' }) expect(callAction).toBeDefined() expect(callAction.verb).toBe(FUNCTION_VERBS.CALL) expect(callAction.data).toMatchObject({ args: { name: 'Alice' }, }) expect(callAction.id).toBeDefined() }) it('links call action to the function Thing', async () => { const fn = defineFunction({ type: 'generative', name: 'process', args: { input: 'Input data' }, output: 'string', }) const thing = await registry.setAsync('process', fn) const callAction = await registry.trackCall('process', { input: 'test' }) // The action's object should be the function thing's ID expect(callAction.object).toBe(thing.id) }) }) describe('trackCompletion()', () => { it('creates an Action for successful completion', async () => { const fn = defineFunction({ type: 'generative', name: 'compute', args: { value: 'Input value' }, output: 'string', }) await registry.setAsync('compute', fn) const callAction = await registry.trackCall('compute', { value: 42 }) const completionAction = await registry.trackCompletion(callAction.id, 'Result: 84', 150) expect(completionAction).toBeDefined() expect(completionAction.verb).toBe(FUNCTION_VERBS.COMPLETE) expect(completionAction.object).toBe(callAction.id) expect(completionAction.data).toMatchObject({ result: 'Result: 84', duration: 150, }) }) it('stores the result in the action data', async () => { const fn = defineFunction({ type: 'generative', name: 'calculate', args: {}, output: 'string', }) await registry.setAsync('calculate', fn) const callAction = await registry.trackCall('calculate', {}) const completionAction = await registry.trackCompletion(callAction.id, { computed: true, value: 100, }) expect(completionAction.data?.result).toEqual({ computed: true, value: 100 }) }) }) describe('trackFailure()', () => { it('creates an Action for failed execution', async () => { const fn = defineFunction({ type: 'generative', name: 'failing', args: {}, output: 'string', }) await registry.setAsync('failing', fn) const callAction = await registry.trackCall('failing', {}) const failureAction = await registry.trackFailure(callAction.id, 'Connection timeout', 5000) expect(failureAction).toBeDefined() expect(failureAction.verb).toBe(FUNCTION_VERBS.FAIL) expect(failureAction.object).toBe(callAction.id) expect(failureAction.data).toMatchObject({ error: 'Connection timeout', duration: 5000, }) }) it('stores error message in action data', async () => { const fn = defineFunction({ type: 'generative', name: 'erroring', args: {}, output: 'string', }) await registry.setAsync('erroring', fn) const callAction = await registry.trackCall('erroring', {}) const failureAction = await registry.trackFailure(callAction.id, 'Invalid input provided') expect(failureAction.data?.error).toBe('Invalid input provided') }) }) describe('getCallHistory()', () => { it('returns calls for a specific function', async () => { const fn = defineFunction({ type: 'generative', name: 'tracked', args: { x: 'Input' }, output: 'string', }) await registry.setAsync('tracked', fn) // Make multiple calls await registry.trackCall('tracked', { x: 1 }) await registry.trackCall('tracked', { x: 2 }) await registry.trackCall('tracked', { x: 3 }) const history = await registry.getCallHistory('tracked') expect(history.length).toBe(3) expect(history.every((a) => a.verb === FUNCTION_VERBS.CALL)).toBe(true) }) it('returns empty array for function with no calls', async () => { const fn = defineFunction({ type: 'generative', name: 'unused', args: {}, output: 'string', }) await registry.setAsync('unused', fn) const history = await registry.getCallHistory('unused') expect(history).toEqual([]) }) it('returns empty array for non-existent function', async () => { const history = await registry.getCallHistory('nonexistent') expect(history).toEqual([]) }) it('only returns calls for the specified function', async () => { const fn1 = defineFunction({ type: 'generative', name: 'funcA', args: {}, output: 'string', }) const fn2 = defineFunction({ type: 'generative', name: 'funcB', args: {}, output: 'string', }) await registry.setAsync('funcA', fn1) await registry.setAsync('funcB', fn2) await registry.trackCall('funcA', { data: 'a1' }) await registry.trackCall('funcA', { data: 'a2' }) await registry.trackCall('funcB', { data: 'b1' }) const historyA = await registry.getCallHistory('funcA') const historyB = await registry.getCallHistory('funcB') expect(historyA.length).toBe(2) expect(historyB.length).toBe(1) }) }) describe('getRecentCalls()', () => { it('returns recent calls across all functions', async () => { const fn1 = defineFunction({ type: 'generative', name: 'recent1', args: {}, output: 'string', }) const fn2 = defineFunction({ type: 'code', name: 'recent2', args: {}, language: 'typescript', }) await registry.setAsync('recent1', fn1) await registry.setAsync('recent2', fn2) await registry.trackCall('recent1', { call: 1 }) await registry.trackCall('recent2', { call: 2 }) await registry.trackCall('recent1', { call: 3 }) const recentCalls = await registry.getRecentCalls() expect(recentCalls.length).toBe(3) expect(recentCalls.every((a) => a.verb === FUNCTION_VERBS.CALL)).toBe(true) }) it('respects the limit parameter', async () => { const fn = defineFunction({ type: 'generative', name: 'limited', args: {}, output: 'string', }) await registry.setAsync('limited', fn) // Make 5 calls for (let i = 0; i < 5; i++) { await registry.trackCall('limited', { index: i }) } const limited = await registry.getRecentCalls(3) expect(limited.length).toBe(3) }) it('defaults to 10 results', async () => { const fn = defineFunction({ type: 'generative', name: 'many', args: {}, output: 'string', }) await registry.setAsync('many', fn) // Make 15 calls for (let i = 0; i < 15; i++) { await registry.trackCall('many', { index: i }) } const recent = await registry.getRecentCalls() expect(recent.length).toBe(10) }) }) describe('getProvider()', () => { it('returns the underlying provider', () => { const returnedProvider = registry.getProvider() expect(returnedProvider).toBe(provider) }) }) }) describe('DigitalObjectsFunctionRegistry - Edge Cases', () => { let provider: DigitalObjectsProvider let registry: DigitalObjectsFunctionRegistry beforeEach(async () => { provider = createMemoryProvider() registry = await createDigitalObjectsRegistry({ provider }) }) it('handles updating an existing function', async () => { const fn1 = defineFunction({ type: 'generative', name: 'evolving', args: { v: 'version 1' }, output: 'string', }) await registry.setAsync('evolving', fn1) // Update with new version const fn2 = defineFunction({ type: 'generative', name: 'evolving', args: { v: 'version 2', extra: 'new field' }, output: 'string', }) await registry.setAsync('evolving', fn2) // Should still have only one Thing const things = await provider.find(FUNCTION_NOUNS.GENERATIVE, { name: 'evolving' }) expect(things.length).toBe(1) // Retrieved function should be the updated one const retrieved = registry.get('evolving') expect(retrieved).toBe(fn2) }) it('handles multiple initializations gracefully', async () => { // Initialize multiple times - should not throw or duplicate nouns await registry.initialize() await registry.initialize() await registry.initialize() const nouns = await provider.listNouns() expect(nouns.length).toBe(4) // Should still be exactly 4 }) it('preserves function type-specific fields', async () => { const humanFn = defineFunction({ type: 'human', name: 'approval', args: { request: 'Request details' }, channel: 'email', instructions: 'Please review and approve', timeout: 86400000, assignee: 'manager@example.com', }) await registry.setAsync('approval', humanFn) // Retrieve from a fresh registry const newRegistry = await createDigitalObjectsRegistry({ provider }) const retrieved = await newRegistry.getAsync('approval') expect(retrieved?.definition.type).toBe('human') const def = retrieved?.definition as { channel: string instructions: string timeout: number assignee: string } expect(def.channel).toBe('email') expect(def.instructions).toBe('Please review and approve') expect(def.timeout).toBe(86400000) expect(def.assignee).toBe('manager@example.com') }) it('tracks define action when creating new function', async () => { const fn = defineFunction({ type: 'generative', name: 'newFunc', args: {}, output: 'string', }) await registry.setAsync('newFunc', fn) // Should have a define action const actions = await provider.listActions({ verb: FUNCTION_VERBS.DEFINE }) expect(actions.length).toBe(1) expect(actions[0].data).toMatchObject({ name: 'newFunc', type: 'generative', }) }) })