ai-functions
Version:
Core AI primitives for building intelligent applications
761 lines (630 loc) • 22.1 kB
text/typescript
/**
* 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',
})
})
})