UNPKG

ai-functions

Version:

Core AI primitives for building intelligent applications

1,081 lines (844 loc) 35.9 kB
/** * Tests for AIPromise module * * Comprehensive tests covering: * - AIPromise class construction and properties * - Promise pipelining behavior * - Property access tracking for schema inference * - Dependency resolution * - Batch map recording and replay * - Streaming interfaces (StreamingAIPromise) * - Error propagation through promise chains * - Factory functions * - Template tag helpers * * @packageDocumentation */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { AIPromise, AI_PROMISE_SYMBOL, RAW_PROMISE_SYMBOL, isAIPromise, getRawPromise, createTextPromise, createObjectPromise, createListPromise, createListsPromise, createBooleanPromise, createExtractPromise, parseTemplateWithDependencies, createAITemplateFunction, type AIPromiseOptions, type StreamingAIPromise, } from '../src/ai-promise.js' import { createBatchMap, BatchMapPromise, isInRecordingMode, getCurrentItemPlaceholder, captureOperation, isBatchMapPromise, BATCH_MAP_SYMBOL, } from '../src/batch-map.js' // Skip integration tests that require AI gateway const hasGateway = !!process.env.AI_GATEWAY_URL || !!process.env.ANTHROPIC_API_KEY // ============================================================================ // 1. AIPromise Class Construction and Identification // ============================================================================ describe('AIPromise Construction and Identification', () => { describe('constructor', () => { it('creates an AIPromise with a prompt', () => { const promise = new AIPromise<string>('test prompt') expect(promise.prompt).toBe('test prompt') }) it('creates an AIPromise with options', () => { const promise = new AIPromise<string>('test prompt', { type: 'text', model: 'sonnet', temperature: 0.7, }) expect(promise.prompt).toBe('test prompt') }) it('initializes with empty property path', () => { const promise = new AIPromise<string>('test prompt') expect(promise.path).toEqual([]) }) it('initializes with provided property path', () => { const promise = new AIPromise<string>('test prompt', { propertyPath: ['foo', 'bar'], }) expect(promise.path).toEqual(['foo', 'bar']) }) it('marks the promise with AI_PROMISE_SYMBOL', () => { const promise = new AIPromise<string>('test prompt') expect((promise as any)[AI_PROMISE_SYMBOL]).toBe(true) }) it('starts with isResolved = false', () => { const promise = new AIPromise<string>('test prompt') expect(promise.isResolved).toBe(false) }) it('starts with empty accessedProps', () => { const promise = new AIPromise<string>('test prompt') expect(promise.accessedProps.size).toBe(0) }) }) describe('isAIPromise helper', () => { it('returns true for AIPromise instances', () => { const promise = new AIPromise<string>('test') expect(isAIPromise(promise)).toBe(true) }) it('returns false for regular promises', () => { const promise = Promise.resolve('test') expect(isAIPromise(promise)).toBe(false) }) it('returns false for null', () => { expect(isAIPromise(null)).toBe(false) }) it('returns false for undefined', () => { expect(isAIPromise(undefined)).toBe(false) }) it('returns false for plain objects', () => { expect(isAIPromise({ test: 'value' })).toBe(false) }) it('returns false for primitives', () => { expect(isAIPromise('string')).toBe(false) expect(isAIPromise(123)).toBe(false) expect(isAIPromise(true)).toBe(false) }) }) describe('getRawPromise helper', () => { it('returns the raw AIPromise from a proxied value', () => { const promise = new AIPromise<{ name: string }>('test') // Access a property to get a proxied child promise const childPromise = (promise as any).name as AIPromise<string> // getRawPromise should return the underlying promise const raw = getRawPromise(childPromise) expect(isAIPromise(raw)).toBe(true) }) it('returns the same promise if already raw', () => { const promise = new AIPromise<string>('test') const raw = getRawPromise(promise) expect(raw.prompt).toBe('test') }) }) }) // ============================================================================ // 2. Property Access Tracking for Schema Inference // ============================================================================ describe('Property Access Tracking', () => { describe('basic property access', () => { it('tracks accessed properties on the promise', () => { const promise = new AIPromise<{ name: string; age: number }>('test') // Access properties through destructuring const { name, age } = promise as any expect(promise.accessedProps.has('name')).toBe(true) expect(promise.accessedProps.has('age')).toBe(true) }) it('tracks nested property access', () => { const promise = new AIPromise<{ user: { name: string } }>('test') // Access nested property const user = (promise as any).user expect(promise.accessedProps.has('user')).toBe(true) }) it('returns a new AIPromise for property access', () => { const promise = new AIPromise<{ name: string }>('test') const nameProp = (promise as any).name expect(isAIPromise(nameProp)).toBe(true) expect(nameProp.path).toEqual(['name']) }) it('builds property path for nested access', () => { const promise = new AIPromise<{ user: { profile: { name: string } } }>('test') const name = (promise as any).user.profile.name expect(name.path).toEqual(['user', 'profile', 'name']) }) }) describe('proxy behavior', () => { it('prevents property mutation', () => { const promise = new AIPromise<{ name: string }>('test') expect(() => { ;(promise as any).name = 'new value' }).toThrow('AIPromise properties are read-only') }) it('prevents property deletion', () => { const promise = new AIPromise<{ name: string }>('test') expect(() => { delete (promise as any).name }).toThrow('AIPromise properties cannot be deleted') }) it('allows access to internal properties', () => { const promise = new AIPromise<string>('test prompt') expect(promise.prompt).toBe('test prompt') expect(promise.path).toEqual([]) expect(promise.isResolved).toBe(false) expect(promise.accessedProps).toBeDefined() }) it('allows access to promise methods', () => { const promise = new AIPromise<string>('test') expect(typeof promise.then).toBe('function') expect(typeof promise.catch).toBe('function') expect(typeof promise.finally).toBe('function') }) it('allows access to AIPromise methods', () => { const promise = new AIPromise<string[]>('test', { type: 'list' }) expect(typeof promise.map).toBe('function') expect(typeof promise.forEach).toBe('function') expect(typeof promise.resolve).toBe('function') expect(typeof promise.stream).toBe('function') expect(typeof promise.addDependency).toBe('function') }) }) }) // ============================================================================ // 3. Promise Interface (then/catch/finally) // ============================================================================ describe('Promise Interface', () => { describe('then()', () => { it('returns a promise from then()', () => { const promise = new AIPromise<string>('test') const result = promise.then((val) => val) expect(result).toBeInstanceOf(Promise) }) it('chains multiple then() calls', () => { const promise = new AIPromise<string>('test') const result = promise.then((val) => val).then((val) => val.length) expect(result).toBeInstanceOf(Promise) }) }) describe('catch()', () => { it('returns a promise from catch()', () => { const promise = new AIPromise<string>('test') const result = promise.catch((err) => 'fallback') expect(result).toBeInstanceOf(Promise) }) }) describe('finally()', () => { it('returns a promise from finally()', () => { const promise = new AIPromise<string>('test') const result = promise.finally(() => {}) expect(result).toBeInstanceOf(Promise) }) it('executes finally callback', async () => { // This is a structural test - we just verify the API works const promise = new AIPromise<string>('test') let finallyCalled = false const result = promise.finally(() => { finallyCalled = true }) expect(result).toBeInstanceOf(Promise) }) }) }) // ============================================================================ // 4. Dependency Management // ============================================================================ describe('Dependency Management', () => { describe('addDependency()', () => { it('adds a dependency to the promise', () => { const promise1 = new AIPromise<string>('first prompt') const promise2 = new AIPromise<string>('second prompt that uses ${dep_0}') promise2.addDependency(promise1) // Dependency is stored internally const deps = (promise2 as any)._dependencies expect(deps.length).toBe(1) expect(deps[0].promise).toBe(promise1) }) it('adds a dependency with a path', () => { const promise1 = new AIPromise<{ name: string }>('first prompt') const promise2 = new AIPromise<string>('second prompt') promise2.addDependency(promise1, ['name']) const deps = (promise2 as any)._dependencies expect(deps.length).toBe(1) expect(deps[0].path).toEqual(['name']) }) it('supports multiple dependencies', () => { const promise1 = new AIPromise<string>('first') const promise2 = new AIPromise<string>('second') const promise3 = new AIPromise<string>('third uses ${dep_0} and ${dep_1}') promise3.addDependency(promise1) promise3.addDependency(promise2) const deps = (promise3 as any)._dependencies expect(deps.length).toBe(2) }) }) }) // ============================================================================ // 5. Factory Functions // ============================================================================ describe('Factory Functions', () => { describe('createTextPromise()', () => { it('creates an AIPromise with text type', () => { const promise = createTextPromise('write about TypeScript') expect(isAIPromise(promise)).toBe(true) expect(promise.prompt).toBe('write about TypeScript') expect((promise as any)._options.type).toBe('text') }) it('accepts options', () => { const promise = createTextPromise('write', { model: 'sonnet' }) expect((promise as any)._options.model).toBe('sonnet') }) }) describe('createObjectPromise()', () => { it('creates an AIPromise with object type', () => { const promise = createObjectPromise<{ name: string }>('generate a person') expect(isAIPromise(promise)).toBe(true) expect((promise as any)._options.type).toBe('object') }) }) describe('createListPromise()', () => { it('creates an AIPromise with list type', () => { const promise = createListPromise('list 5 colors') expect(isAIPromise(promise)).toBe(true) expect((promise as any)._options.type).toBe('list') }) }) describe('createListsPromise()', () => { it('creates an AIPromise with lists type', () => { const promise = createListsPromise('pros and cons of TypeScript') expect(isAIPromise(promise)).toBe(true) expect((promise as any)._options.type).toBe('lists') }) }) describe('createBooleanPromise()', () => { it('creates an AIPromise with boolean type', () => { const promise = createBooleanPromise('is TypeScript better than JavaScript?') expect(isAIPromise(promise)).toBe(true) expect((promise as any)._options.type).toBe('boolean') }) }) describe('createExtractPromise()', () => { it('creates an AIPromise with extract type', () => { const promise = createExtractPromise<string>('extract names from text') expect(isAIPromise(promise)).toBe(true) expect((promise as any)._options.type).toBe('extract') }) }) }) // ============================================================================ // 6. Template Tag Helpers // ============================================================================ describe('Template Tag Helpers', () => { describe('parseTemplateWithDependencies()', () => { it('parses a simple template without dependencies', () => { const strings = ['Hello, world!'] as unknown as TemplateStringsArray Object.defineProperty(strings, 'raw', { value: ['Hello, world!'] }) const result = parseTemplateWithDependencies(strings) expect(result.prompt).toBe('Hello, world!') expect(result.dependencies).toEqual([]) }) it('parses a template with string interpolation', () => { const strings = ['Hello, ', '!'] as unknown as TemplateStringsArray Object.defineProperty(strings, 'raw', { value: ['Hello, ', '!'] }) const result = parseTemplateWithDependencies(strings, 'World') expect(result.prompt).toBe('Hello, World!') expect(result.dependencies).toEqual([]) }) it('parses a template with AIPromise dependency', () => { const strings = ['The topic is: ', ' - please expand'] as unknown as TemplateStringsArray Object.defineProperty(strings, 'raw', { value: strings }) const aiPromise = new AIPromise<string>('generate a topic') const result = parseTemplateWithDependencies(strings, aiPromise) expect(result.prompt).toContain('${dep_0}') expect(result.dependencies.length).toBe(1) expect(result.dependencies[0].promise.prompt).toBe('generate a topic') }) it('handles multiple AIPromise dependencies', () => { const strings = [ 'Combine ', ' with ', ' to create something new', ] as unknown as TemplateStringsArray Object.defineProperty(strings, 'raw', { value: strings }) const promise1 = new AIPromise<string>('first topic') const promise2 = new AIPromise<string>('second topic') const result = parseTemplateWithDependencies(strings, promise1, promise2) expect(result.dependencies.length).toBe(2) expect(result.prompt).toContain('${dep_0}') expect(result.prompt).toContain('${dep_1}') }) it('handles mixed interpolation (strings and AIPromises)', () => { const strings = [ 'Write about ', ' by ', ' in style of ', '', ] as unknown as TemplateStringsArray Object.defineProperty(strings, 'raw', { value: strings }) const topicPromise = new AIPromise<string>('generate topic') const result = parseTemplateWithDependencies( strings, topicPromise, 'Author Name', 'Hemingway' ) expect(result.dependencies.length).toBe(1) expect(result.prompt).toContain('Author Name') expect(result.prompt).toContain('Hemingway') }) }) describe('createAITemplateFunction()', () => { it('creates a template function for text type', () => { const templateFn = createAITemplateFunction<string>('text') expect(typeof templateFn).toBe('function') }) it('works as tagged template literal', () => { const templateFn = createAITemplateFunction<string>('text') const result = templateFn`Write about TypeScript` expect(isAIPromise(result)).toBe(true) }) it('works as regular function call', () => { const templateFn = createAITemplateFunction<string>('text') const result = templateFn('Write about TypeScript') expect(isAIPromise(result)).toBe(true) }) it('accepts options in function call mode', () => { const templateFn = createAITemplateFunction<string>('text') const result = templateFn('Write about TypeScript', { model: 'claude-opus-4-5' }) expect((result as any)._options.model).toBe('claude-opus-4-5') }) it('tracks dependencies from template interpolation', () => { const templateFn = createAITemplateFunction<string>('text') const topicPromise = new AIPromise<string>('generate topic') const result = templateFn`Write about ${topicPromise}` const deps = (result as any)._dependencies expect(deps.length).toBe(1) }) it('creates boolean type promises', () => { const isFn = createAITemplateFunction<boolean>('boolean') const result = isFn`Is TypeScript better than JavaScript?` expect((result as any)._options.type).toBe('boolean') }) it('creates list type promises', () => { const listFn = createAITemplateFunction<string[]>('list') const result = listFn`5 programming languages` expect((result as any)._options.type).toBe('list') }) }) }) // ============================================================================ // 7. Map Operations // ============================================================================ describe('Map Operations', () => { describe('map()', () => { it('returns a BatchMapPromise', () => { const promise = new AIPromise<string[]>('list items', { type: 'list' }) const mapped = promise.map((item) => `processed: ${item}`) expect(isBatchMapPromise(mapped)).toBe(true) }) it('map() method exists on list promises', () => { const promise = createListPromise('list 5 colors') expect(typeof promise.map).toBe('function') }) }) describe('mapImmediate()', () => { it('returns a BatchMapPromise with immediate option', () => { const promise = new AIPromise<string[]>('list items', { type: 'list' }) const mapped = promise.mapImmediate((item) => `processed: ${item}`) expect(isBatchMapPromise(mapped)).toBe(true) expect((mapped as any)._options.immediate).toBe(true) }) }) describe('mapDeferred()', () => { it('returns a BatchMapPromise with deferred option', () => { const promise = new AIPromise<string[]>('list items', { type: 'list' }) const mapped = promise.mapDeferred((item) => `processed: ${item}`) expect(isBatchMapPromise(mapped)).toBe(true) expect((mapped as any)._options.deferred).toBe(true) }) }) }) // ============================================================================ // 8. forEach Operations // ============================================================================ describe('forEach Operations', () => { describe('forEach()', () => { it('forEach method exists on AIPromise', () => { const promise = new AIPromise<string[]>('list items', { type: 'list' }) expect(typeof promise.forEach).toBe('function') }) }) }) // ============================================================================ // 9. AsyncIterator Support // ============================================================================ describe('AsyncIterator Support', () => { it('AIPromise has Symbol.asyncIterator', () => { const promise = new AIPromise<string[]>('list items', { type: 'list' }) expect(typeof promise[Symbol.asyncIterator]).toBe('function') }) }) // ============================================================================ // 10. Streaming Interface // ============================================================================ describe('Streaming Interface', () => { describe('stream()', () => { it('returns a StreamingAIPromise', () => { const promise = new AIPromise<string>('test prompt', { type: 'text' }) const stream = promise.stream() expect(stream).toBeDefined() expect(typeof stream[Symbol.asyncIterator]).toBe('function') }) it('stream has textStream property', () => { const promise = new AIPromise<string>('test prompt', { type: 'text' }) const stream = promise.stream() expect(stream.textStream).toBeDefined() expect(typeof stream.textStream[Symbol.asyncIterator]).toBe('function') }) it('stream has partialObjectStream property', () => { const promise = new AIPromise<{ name: string }>('test prompt', { type: 'object' }) const stream = promise.stream() expect(stream.partialObjectStream).toBeDefined() expect(typeof stream.partialObjectStream[Symbol.asyncIterator]).toBe('function') }) it('stream has result promise', () => { const promise = new AIPromise<string>('test prompt', { type: 'text' }) const stream = promise.stream() expect(stream.result).toBeDefined() expect(typeof stream.result.then).toBe('function') }) it('stream is thenable', () => { const promise = new AIPromise<string>('test prompt', { type: 'text' }) const stream = promise.stream() expect(typeof stream.then).toBe('function') }) it('accepts abort signal option', () => { const promise = new AIPromise<string>('test prompt', { type: 'text' }) const controller = new AbortController() const stream = promise.stream({ abortSignal: controller.signal }) expect(stream).toBeDefined() }) }) }) // ============================================================================ // 11. Batch Map Integration // ============================================================================ describe('Batch Map Integration', () => { describe('BatchMapPromise', () => { it('can be created with items and operations', () => { const batchMap = new BatchMapPromise<string>(['a', 'b', 'c'], [], {}) expect(batchMap.size).toBe(3) }) it('has BATCH_MAP_SYMBOL', () => { const batchMap = new BatchMapPromise<string>([], [], {}) expect((batchMap as any)[BATCH_MAP_SYMBOL]).toBe(true) }) it('isBatchMapPromise returns true for BatchMapPromise', () => { const batchMap = new BatchMapPromise<string>([], [], {}) expect(isBatchMapPromise(batchMap)).toBe(true) }) it('isBatchMapPromise returns false for regular promises', () => { expect(isBatchMapPromise(Promise.resolve([]))).toBe(false) }) it('isBatchMapPromise returns false for AIPromise', () => { const promise = new AIPromise<string[]>('test', { type: 'list' }) expect(isBatchMapPromise(promise)).toBe(false) }) }) describe('Recording Mode', () => { it('isInRecordingMode returns false outside of batch map', () => { expect(isInRecordingMode()).toBe(false) }) it('getCurrentItemPlaceholder returns null outside of batch map', () => { expect(getCurrentItemPlaceholder()).toBe(null) }) }) describe('createBatchMap()', () => { it('creates a BatchMapPromise from items and callback', () => { const items = ['a', 'b', 'c'] const callback = (item: string) => item.toUpperCase() const batchMap = createBatchMap(items, callback) expect(isBatchMapPromise(batchMap)).toBe(true) expect(batchMap.size).toBe(3) }) it('accepts options', () => { const items = ['a', 'b'] const callback = (item: string) => item const batchMap = createBatchMap(items, callback, { immediate: true }) expect((batchMap as any)._options.immediate).toBe(true) }) }) }) // ============================================================================ // 12. Schema Building // ============================================================================ describe('Schema Building', () => { describe('_buildSchema() internal method', () => { it('uses base schema when no properties accessed', () => { const promise = new AIPromise<{ name: string }>('test', { baseSchema: { name: 'The person name' }, }) const schema = (promise as any)._buildSchema() expect(schema).toEqual({ name: 'The person name' }) }) it('infers list type from property name ending in s', () => { const promise = new AIPromise<{ items: string[] }>('test') // Access the property to track it const { items } = promise as any const schema = (promise as any)._buildSchema() expect(Array.isArray(schema.items)).toBe(true) }) it('infers boolean type from property name patterns', () => { const promise = new AIPromise<{ isValid: boolean; hasError: boolean }>('test') // Access the properties const { isValid, hasError } = promise as any const schema = (promise as any)._buildSchema() expect(schema.isValid).toContain('true/false') expect(schema.hasError).toContain('true/false') }) it('infers number type from property name patterns', () => { const promise = new AIPromise<{ count: number; totalAmount: number }>('test') // Access the properties const { count, totalAmount } = promise as any const schema = (promise as any)._buildSchema() expect(schema.count).toContain('number') expect(schema.totalAmount).toContain('number') }) it('returns default schema for text type', () => { const promise = new AIPromise<string>('test', { type: 'text' }) const schema = (promise as any)._buildSchema() expect(schema).toHaveProperty('text') }) it('returns default schema for list type', () => { const promise = new AIPromise<string[]>('test', { type: 'list' }) const schema = (promise as any)._buildSchema() expect(schema).toHaveProperty('items') expect(Array.isArray(schema.items)).toBe(true) }) it('returns default schema for boolean type', () => { const promise = new AIPromise<boolean>('test', { type: 'boolean' }) const schema = (promise as any)._buildSchema() expect(schema).toHaveProperty('answer') }) it('returns default schema for extract type', () => { const promise = new AIPromise<string[]>('test', { type: 'extract' }) const schema = (promise as any)._buildSchema() expect(schema).toHaveProperty('items') }) it('returns default schema for lists type', () => { const promise = new AIPromise<Record<string, string[]>>('test', { type: 'lists' }) const schema = (promise as any)._buildSchema() expect(schema).toHaveProperty('categories') expect(schema).toHaveProperty('data') }) }) }) // ============================================================================ // 13. Error Handling // ============================================================================ describe('Error Handling', () => { describe('proxy errors', () => { it('throws on set attempt', () => { const promise = new AIPromise<{ name: string }>('test') expect(() => { ;(promise as any).name = 'value' }).toThrow('read-only') }) it('throws on delete attempt', () => { const promise = new AIPromise<{ name: string }>('test') expect(() => { delete (promise as any).name }).toThrow('cannot be deleted') }) }) describe('resolution errors', () => { it('catch catches rejection from promise chain', async () => { const promise = new AIPromise<string>('test') // Test that catch method works correctly by chaining const result = promise .then(() => { throw new Error('Test error') }) .catch((err) => 'caught') // This tests the catch method behavior - it should handle the thrown error expect(result).toBeInstanceOf(Promise) }) it('finally is called regardless of resolution', async () => { const promise = new AIPromise<string>('test') let finallyCalled = false const result = promise.finally(() => { finallyCalled = true }) // finally() should return a promise expect(result).toBeInstanceOf(Promise) }) }) }) // ============================================================================ // 14. Parent-Child Promise Relationships // ============================================================================ describe('Parent-Child Promise Relationships', () => { it('child promise has reference to parent', () => { const parent = new AIPromise<{ name: string }>('test parent prompt') const child = (parent as any).name // Get the raw promise to access _parent const rawChild = getRawPromise(child) const rawParent = getRawPromise(parent) // Check that the child's parent has the same prompt as the parent expect((rawChild as any)._parent?.prompt).toBe('test parent prompt') }) it('child promise has correct property path', () => { const parent = new AIPromise<{ user: { name: string } }>('test') const name = (parent as any).user.name expect(name.path).toEqual(['user', 'name']) }) it('deeply nested access builds complete path', () => { const promise = new AIPromise<{ a: { b: { c: { d: string } } } }>('test') const deep = (promise as any).a.b.c.d expect(deep.path).toEqual(['a', 'b', 'c', 'd']) }) }) // ============================================================================ // 15. Promise Pipelining (without await) // ============================================================================ describe('Promise Pipelining', () => { it('allows chaining without await', () => { // Create a chain of promises const topic = new AIPromise<string>('generate a topic', { type: 'text' }) const essay = new AIPromise<string>('write about the topic', { type: 'text' }) // Add dependency essay.addDependency(topic) // The chain exists without any await expect(isAIPromise(topic)).toBe(true) expect(isAIPromise(essay)).toBe(true) expect((essay as any)._dependencies.length).toBe(1) }) it('property access creates dependent promises', () => { const promise = new AIPromise<{ title: string; body: string }>('generate article') // Access properties - creates child promises const { title, body } = promise as any // Both are AIPromises expect(isAIPromise(title)).toBe(true) expect(isAIPromise(body)).toBe(true) // Both have parent reference expect((title as any)._parent.prompt).toBe('generate article') expect((body as any)._parent.prompt).toBe('generate article') }) }) // ============================================================================ // 16. Type-Specific Behavior Tests // ============================================================================ describe('Type-Specific Behavior', () => { describe('text type', () => { it('creates promise with text type', () => { const promise = createTextPromise('write a haiku') expect((promise as any)._options.type).toBe('text') }) }) describe('object type', () => { it('creates promise with object type', () => { const promise = createObjectPromise('generate person data') expect((promise as any)._options.type).toBe('object') }) it('tracks accessed properties for schema', () => { const promise = createObjectPromise<{ name: string; age: number }>('person') const { name, age } = promise as any expect(promise.accessedProps.has('name')).toBe(true) expect(promise.accessedProps.has('age')).toBe(true) }) }) describe('list type', () => { it('creates promise with list type', () => { const promise = createListPromise('5 colors') expect((promise as any)._options.type).toBe('list') }) it('supports map operations', () => { const promise = createListPromise('3 numbers') expect(typeof promise.map).toBe('function') }) }) describe('boolean type', () => { it('creates promise with boolean type', () => { const promise = createBooleanPromise('is it true?') expect((promise as any)._options.type).toBe('boolean') }) }) describe('extract type', () => { it('creates promise with extract type', () => { const promise = createExtractPromise('extract names') expect((promise as any)._options.type).toBe('extract') }) }) describe('lists type', () => { it('creates promise with lists type', () => { const promise = createListsPromise('pros and cons') expect((promise as any)._options.type).toBe('lists') }) }) }) // ============================================================================ // 17. Resolution State Management // ============================================================================ describe('Resolution State Management', () => { it('starts unresolved', () => { const promise = new AIPromise<string>('test') expect(promise.isResolved).toBe(false) }) it('caches resolver promise', () => { const promise = new AIPromise<string>('test') // Call then twice const result1 = promise.then((v) => v) const result2 = promise.then((v) => v) // Both should use the same resolver // (We can't directly test the internal _resolver, but the behavior should be consistent) expect(result1).toBeInstanceOf(Promise) expect(result2).toBeInstanceOf(Promise) }) }) // ============================================================================ // Integration Tests (Real AI calls) // ============================================================================ describe.skipIf(!hasGateway)('AIPromise Integration Tests', () => { describe('Basic Resolution', () => { it('resolves a text promise', async () => { const promise = createTextPromise('Say hello in exactly 2 words') const result = await promise expect(typeof result).toBe('string') expect(result.length).toBeGreaterThan(0) }, 60000) it('resolves an object promise with accessed properties', async () => { const promise = createObjectPromise<{ greeting: string; language: string }>( 'Generate a greeting in French' ) // Access properties to define schema const { greeting, language } = promise as any const result = await promise expect(result).toHaveProperty('greeting') expect(result).toHaveProperty('language') }, 60000) it('resolves a list promise', async () => { const promise = createListPromise('List exactly 3 primary colors') const result = await promise expect(Array.isArray(result)).toBe(true) expect(result.length).toBeGreaterThanOrEqual(3) }, 60000) it('resolves a boolean promise', async () => { const promise = createBooleanPromise('Is the sky blue on a clear day?') const result = await promise expect(typeof result).toBe('boolean') expect(result).toBe(true) }, 60000) }) describe('Property Access Resolution', () => { it('resolves child property from parent', async () => { const promise = createObjectPromise<{ name: string; age: number }>( 'Generate a fictional person with name John and age 30' ) // Access properties const { name } = promise as any // Resolve the parent first const parent = await promise // The child should resolve to the parent's property expect(parent.name).toBeDefined() }, 60000) }) describe('Dependency Resolution', () => { it('resolves dependencies before main promise', async () => { const topicPromise = createTextPromise('Pick a topic: science or art. Just say one word.') const essayPromise = createTextPromise('Write one sentence about ${dep_0}') essayPromise.addDependency(topicPromise) const result = await essayPromise expect(typeof result).toBe('string') expect(result.length).toBeGreaterThan(0) }, 120000) }) })