ai-functions 2.1.1 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -4
- package/CHANGELOG.md +68 -1
- package/README.md +397 -157
- package/dist/ai-promise.d.ts +50 -3
- package/dist/ai-promise.d.ts.map +1 -1
- package/dist/ai-promise.js +410 -51
- package/dist/ai-promise.js.map +1 -1
- package/dist/ai-schemas.d.ts +56 -0
- package/dist/ai-schemas.d.ts.map +1 -0
- package/dist/ai-schemas.js +53 -0
- package/dist/ai-schemas.js.map +1 -0
- package/dist/ai.d.ts +16 -242
- package/dist/ai.d.ts.map +1 -1
- package/dist/ai.js +54 -837
- package/dist/ai.js.map +1 -1
- package/dist/batch/anthropic.d.ts +6 -4
- package/dist/batch/anthropic.d.ts.map +1 -1
- package/dist/batch/anthropic.js +83 -145
- package/dist/batch/anthropic.js.map +1 -1
- package/dist/batch/bedrock.d.ts +8 -30
- package/dist/batch/bedrock.d.ts.map +1 -1
- package/dist/batch/bedrock.js +155 -338
- package/dist/batch/bedrock.js.map +1 -1
- package/dist/batch/cloudflare.d.ts +8 -20
- package/dist/batch/cloudflare.d.ts.map +1 -1
- package/dist/batch/cloudflare.js +68 -189
- package/dist/batch/cloudflare.js.map +1 -1
- package/dist/batch/google.d.ts +6 -20
- package/dist/batch/google.d.ts.map +1 -1
- package/dist/batch/google.js +70 -238
- package/dist/batch/google.js.map +1 -1
- package/dist/batch/index.d.ts +4 -1
- package/dist/batch/index.d.ts.map +1 -1
- package/dist/batch/index.js +4 -1
- package/dist/batch/index.js.map +1 -1
- package/dist/batch/memory.d.ts +1 -1
- package/dist/batch/memory.d.ts.map +1 -1
- package/dist/batch/memory.js +14 -10
- package/dist/batch/memory.js.map +1 -1
- package/dist/batch/openai.d.ts +11 -14
- package/dist/batch/openai.d.ts.map +1 -1
- package/dist/batch/openai.js +52 -156
- package/dist/batch/openai.js.map +1 -1
- package/dist/batch/provider.d.ts +111 -0
- package/dist/batch/provider.d.ts.map +1 -0
- package/dist/batch/provider.js +233 -0
- package/dist/batch/provider.js.map +1 -0
- package/dist/batch-map.d.ts.map +1 -1
- package/dist/batch-map.js +23 -17
- package/dist/batch-map.js.map +1 -1
- package/dist/batch-queue.d.ts +65 -0
- package/dist/batch-queue.d.ts.map +1 -1
- package/dist/batch-queue.js +169 -14
- package/dist/batch-queue.js.map +1 -1
- package/dist/budget.d.ts +272 -0
- package/dist/budget.d.ts.map +1 -0
- package/dist/budget.js +513 -0
- package/dist/budget.js.map +1 -0
- package/dist/cache.d.ts +295 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +433 -0
- package/dist/cache.js.map +1 -0
- package/dist/context.d.ts +42 -8
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +64 -62
- package/dist/context.js.map +1 -1
- package/dist/digital-objects-registry.d.ts +229 -0
- package/dist/digital-objects-registry.d.ts.map +1 -0
- package/dist/digital-objects-registry.js +617 -0
- package/dist/digital-objects-registry.js.map +1 -0
- package/dist/embeddings.d.ts +2 -2
- package/dist/embeddings.d.ts.map +1 -1
- package/dist/errors.d.ts +22 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +35 -0
- package/dist/errors.js.map +1 -0
- package/dist/eval/runner.d.ts +10 -1
- package/dist/eval/runner.d.ts.map +1 -1
- package/dist/eval/runner.js +41 -35
- package/dist/eval/runner.js.map +1 -1
- package/dist/eval-log/in-memory.d.ts +34 -0
- package/dist/eval-log/in-memory.d.ts.map +1 -0
- package/dist/eval-log/in-memory.js +84 -0
- package/dist/eval-log/in-memory.js.map +1 -0
- package/dist/eval-log/index.d.ts +29 -0
- package/dist/eval-log/index.d.ts.map +1 -0
- package/dist/eval-log/index.js +39 -0
- package/dist/eval-log/index.js.map +1 -0
- package/dist/eval-log/types.d.ts +101 -0
- package/dist/eval-log/types.d.ts.map +1 -0
- package/dist/eval-log/types.js +16 -0
- package/dist/eval-log/types.js.map +1 -0
- package/dist/function-registry.d.ts +116 -0
- package/dist/function-registry.d.ts.map +1 -0
- package/dist/function-registry.js +546 -0
- package/dist/function-registry.js.map +1 -0
- package/dist/generate.d.ts +9 -3
- package/dist/generate.d.ts.map +1 -1
- package/dist/generate.js +18 -22
- package/dist/generate.js.map +1 -1
- package/dist/index.d.ts +35 -20
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +89 -42
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +118 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +187 -0
- package/dist/logger.js.map +1 -0
- package/dist/middleware/budget.d.ts +84 -0
- package/dist/middleware/budget.d.ts.map +1 -0
- package/dist/middleware/budget.js +110 -0
- package/dist/middleware/budget.js.map +1 -0
- package/dist/middleware/cache.d.ts +103 -0
- package/dist/middleware/cache.d.ts.map +1 -0
- package/dist/middleware/cache.js +228 -0
- package/dist/middleware/cache.js.map +1 -0
- package/dist/middleware/embed-cache.d.ts +99 -0
- package/dist/middleware/embed-cache.d.ts.map +1 -0
- package/dist/middleware/embed-cache.js +128 -0
- package/dist/middleware/embed-cache.js.map +1 -0
- package/dist/middleware/index.d.ts +11 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +11 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/trace.d.ts +103 -0
- package/dist/middleware/trace.d.ts.map +1 -0
- package/dist/middleware/trace.js +176 -0
- package/dist/middleware/trace.js.map +1 -0
- package/dist/primitives.d.ts +120 -1
- package/dist/primitives.d.ts.map +1 -1
- package/dist/primitives.js +398 -26
- package/dist/primitives.js.map +1 -1
- package/dist/retry.d.ts +368 -0
- package/dist/retry.d.ts.map +1 -0
- package/dist/retry.js +646 -0
- package/dist/retry.js.map +1 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +2 -10
- package/dist/schema.js.map +1 -1
- package/dist/telemetry.d.ts +128 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +285 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/template.d.ts.map +1 -1
- package/dist/template.js +6 -1
- package/dist/template.js.map +1 -1
- package/dist/tool-orchestration.d.ts +453 -0
- package/dist/tool-orchestration.d.ts.map +1 -0
- package/dist/tool-orchestration.js +763 -0
- package/dist/tool-orchestration.js.map +1 -0
- package/dist/type-guards.d.ts +28 -0
- package/dist/type-guards.d.ts.map +1 -0
- package/dist/type-guards.js +29 -0
- package/dist/type-guards.js.map +1 -0
- package/dist/types.d.ts +135 -17
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +36 -1
- package/dist/types.js.map +1 -1
- package/dist/wrap-for-v3.d.ts +80 -0
- package/dist/wrap-for-v3.d.ts.map +1 -0
- package/dist/wrap-for-v3.js +89 -0
- package/dist/wrap-for-v3.js.map +1 -0
- package/examples/00-quickstart.ts +232 -0
- package/examples/01-rag-chatbot.ts +212 -0
- package/examples/02-multi-agent-research.ts +290 -0
- package/examples/03-email-classification.ts +379 -0
- package/examples/04-content-moderation.ts +400 -0
- package/examples/05-document-extraction.ts +455 -0
- package/examples/06-streaming-chat-nextjs.ts +437 -0
- package/examples/07-cloudflare-worker.ts +483 -0
- package/examples/08-batch-processing.ts +491 -0
- package/examples/09-budget-constrained.ts +527 -0
- package/examples/10-tool-orchestration.ts +565 -0
- package/examples/11-retry-resilience.ts +403 -0
- package/examples/12-caching-strategies.ts +422 -0
- package/examples/README.md +145 -0
- package/package.json +10 -6
- package/src/ai-promise.ts +528 -99
- package/src/ai-schemas.ts +122 -0
- package/src/ai.ts +69 -1153
- package/src/batch/anthropic.ts +96 -161
- package/src/batch/bedrock.ts +203 -454
- package/src/batch/cloudflare.ts +99 -282
- package/src/batch/google.ts +91 -297
- package/src/batch/index.ts +4 -1
- package/src/batch/memory.ts +15 -10
- package/src/batch/openai.ts +65 -193
- package/src/batch/provider.ts +336 -0
- package/src/batch-map.ts +29 -24
- package/src/batch-queue.ts +200 -11
- package/src/budget.ts +740 -0
- package/src/cache.ts +681 -0
- package/src/context.ts +122 -76
- package/src/digital-objects-registry.ts +750 -0
- package/src/errors.ts +37 -0
- package/src/eval/runner.ts +63 -38
- package/src/eval-log/in-memory.ts +90 -0
- package/src/eval-log/index.ts +46 -0
- package/src/eval-log/types.ts +110 -0
- package/src/function-registry.ts +671 -0
- package/src/generate.ts +33 -33
- package/src/index.ts +325 -49
- package/src/logger.ts +232 -0
- package/src/middleware/budget.ts +171 -0
- package/src/middleware/cache.ts +299 -0
- package/src/middleware/embed-cache.ts +195 -0
- package/src/middleware/index.ts +23 -0
- package/src/middleware/trace.ts +248 -0
- package/src/primitives.ts +589 -62
- package/src/retry.ts +902 -0
- package/src/schema.ts +8 -17
- package/src/telemetry.ts +403 -0
- package/src/template.ts +8 -4
- package/src/tool-orchestration.ts +1173 -0
- package/src/type-guards.ts +31 -0
- package/src/types.ts +164 -25
- package/src/wrap-for-v3.ts +105 -0
- package/test/ai-promise.test.ts +1080 -0
- package/test/ai-proxy.test.ts +1 -1
- package/test/backward-compat.test.ts +147 -0
- package/test/batch-autosubmit-errors.test.ts +610 -0
- package/test/batch-blog-posts.test.ts +87 -129
- package/test/budget-tracking.test.ts +800 -0
- package/test/cache.test.ts +712 -0
- package/test/context-isolation.test.ts +687 -0
- package/test/core-functions.test.ts +183 -579
- package/test/decide.test.ts +154 -322
- package/test/define.test.ts +211 -8
- package/test/digital-objects-registry.test.ts +760 -0
- package/test/embedding-cache-middleware.test.ts +140 -0
- package/test/evals/deterministic.eval.test.ts +376 -0
- package/test/generate-core.test.ts +140 -229
- package/test/implicit-batch.test.ts +22 -65
- package/test/json-parse-error-handling.test.ts +463 -0
- package/test/retry-policy-integration.test.ts +117 -0
- package/test/retry.test.ts +1016 -0
- package/test/schema.test.ts +55 -19
- package/test/streaming.test.ts +316 -0
- package/test/template.test.ts +1164 -0
- package/test/tool-orchestration.test.ts +1040 -0
- package/test/wrap-for-v3.test.ts +612 -0
- package/vitest.config.js +6 -0
- package/vitest.config.ts +20 -0
- package/dist/rpc/auth.d.ts +0 -69
- package/dist/rpc/auth.d.ts.map +0 -1
- package/dist/rpc/auth.js +0 -136
- package/dist/rpc/auth.js.map +0 -1
- package/dist/rpc/client.d.ts +0 -62
- package/dist/rpc/client.d.ts.map +0 -1
- package/dist/rpc/client.js +0 -103
- package/dist/rpc/client.js.map +0 -1
- package/dist/rpc/deferred.d.ts +0 -60
- package/dist/rpc/deferred.d.ts.map +0 -1
- package/dist/rpc/deferred.js +0 -96
- package/dist/rpc/deferred.js.map +0 -1
- package/dist/rpc/index.d.ts +0 -22
- package/dist/rpc/index.d.ts.map +0 -1
- package/dist/rpc/index.js +0 -38
- package/dist/rpc/index.js.map +0 -1
- package/dist/rpc/local.d.ts +0 -42
- package/dist/rpc/local.d.ts.map +0 -1
- package/dist/rpc/local.js +0 -50
- package/dist/rpc/local.js.map +0 -1
- package/dist/rpc/server.d.ts +0 -165
- package/dist/rpc/server.d.ts.map +0 -1
- package/dist/rpc/server.js +0 -405
- package/dist/rpc/server.js.map +0 -1
- package/dist/rpc/session.d.ts +0 -32
- package/dist/rpc/session.d.ts.map +0 -1
- package/dist/rpc/session.js +0 -43
- package/dist/rpc/session.js.map +0 -1
- package/dist/rpc/transport.d.ts +0 -306
- package/dist/rpc/transport.d.ts.map +0 -1
- package/dist/rpc/transport.js +0 -731
- package/dist/rpc/transport.js.map +0 -1
- package/src/batch/anthropic.js +0 -256
- package/src/batch/bedrock.js +0 -584
- package/src/batch/cloudflare.js +0 -287
- package/src/batch/google.js +0 -359
- package/src/batch/index.js +0 -30
- package/src/batch/memory.js +0 -187
- package/src/batch/openai.js +0 -402
- package/src/eval/index.js +0 -7
- package/src/eval/models.js +0 -119
- package/src/eval/runner.js +0 -147
- package/test/schema.test.js +0 -96
|
@@ -0,0 +1,1164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for template.ts module
|
|
3
|
+
*
|
|
4
|
+
* This module provides core utilities for all AI functions:
|
|
5
|
+
* - parseTemplate: Parse tagged template literals with YAML conversion
|
|
6
|
+
* - createChainablePromise: Create promises that support options chaining
|
|
7
|
+
* - createTemplateFunction: Create functions supporting both template and regular calls
|
|
8
|
+
* - withBatch: Add batch capability to template functions
|
|
9
|
+
* - createAsyncIterable: Create async iterables from arrays or generators
|
|
10
|
+
* - createStreamableList: Create dual Promise/AsyncIterable results
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
14
|
+
import {
|
|
15
|
+
parseTemplate,
|
|
16
|
+
createChainablePromise,
|
|
17
|
+
createTemplateFunction,
|
|
18
|
+
withBatch,
|
|
19
|
+
createAsyncIterable,
|
|
20
|
+
createStreamableList,
|
|
21
|
+
type FunctionOptions,
|
|
22
|
+
type ChainablePromise,
|
|
23
|
+
type TemplateFunction,
|
|
24
|
+
type BatchableFunction,
|
|
25
|
+
type StreamableList,
|
|
26
|
+
} from '../src/template.js'
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// parseTemplate Tests
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
describe('parseTemplate', () => {
|
|
33
|
+
describe('basic interpolation', () => {
|
|
34
|
+
it('parses simple string interpolation', () => {
|
|
35
|
+
const topic = 'TypeScript'
|
|
36
|
+
const result = parseTemplate`Write about ${topic}`
|
|
37
|
+
expect(result).toBe('Write about TypeScript')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('parses multiple interpolations', () => {
|
|
41
|
+
const topic = 'TypeScript'
|
|
42
|
+
const audience = 'beginners'
|
|
43
|
+
const result = parseTemplate`Write about ${topic} for ${audience}`
|
|
44
|
+
expect(result).toBe('Write about TypeScript for beginners')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('handles empty template', () => {
|
|
48
|
+
const result = parseTemplate``
|
|
49
|
+
expect(result).toBe('')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('handles template with no interpolations', () => {
|
|
53
|
+
const result = parseTemplate`Just plain text`
|
|
54
|
+
expect(result).toBe('Just plain text')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('handles adjacent interpolations', () => {
|
|
58
|
+
const a = 'Hello'
|
|
59
|
+
const b = 'World'
|
|
60
|
+
const result = parseTemplate`${a}${b}`
|
|
61
|
+
expect(result).toBe('HelloWorld')
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('primitive types', () => {
|
|
66
|
+
it('handles numbers', () => {
|
|
67
|
+
const count = 42
|
|
68
|
+
const result = parseTemplate`Generate ${count} items`
|
|
69
|
+
expect(result).toBe('Generate 42 items')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('handles booleans', () => {
|
|
73
|
+
const active = true
|
|
74
|
+
const inactive = false
|
|
75
|
+
const result = parseTemplate`Active: ${active}, Inactive: ${inactive}`
|
|
76
|
+
expect(result).toBe('Active: true, Inactive: false')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('handles zero', () => {
|
|
80
|
+
const zero = 0
|
|
81
|
+
const result = parseTemplate`Count: ${zero}`
|
|
82
|
+
expect(result).toBe('Count: 0')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('handles empty string', () => {
|
|
86
|
+
const empty = ''
|
|
87
|
+
const result = parseTemplate`Value: ${empty}!`
|
|
88
|
+
expect(result).toBe('Value: !')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('handles BigInt', () => {
|
|
92
|
+
const big = BigInt(9007199254740991)
|
|
93
|
+
const result = parseTemplate`Big number: ${big}`
|
|
94
|
+
expect(result).toBe('Big number: 9007199254740991')
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
describe('null and undefined handling', () => {
|
|
99
|
+
it('handles undefined at end (trailing template part)', () => {
|
|
100
|
+
const value = undefined
|
|
101
|
+
const result = parseTemplate`Value is ${value}`
|
|
102
|
+
expect(result).toBe('Value is ')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('handles undefined in middle', () => {
|
|
106
|
+
const value = undefined
|
|
107
|
+
const result = parseTemplate`Before ${value} after`
|
|
108
|
+
expect(result).toBe('Before after')
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('handles null as object (converts to YAML)', () => {
|
|
112
|
+
const value = null
|
|
113
|
+
const result = parseTemplate`Value is ${value}`
|
|
114
|
+
// null is typeof 'object' but === null, so it goes to String()
|
|
115
|
+
expect(result).toContain('Value is')
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('handles multiple undefined values', () => {
|
|
119
|
+
const a = undefined
|
|
120
|
+
const b = undefined
|
|
121
|
+
const result = parseTemplate`${a} and ${b}`
|
|
122
|
+
expect(result).toBe(' and ')
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
describe('object and array YAML conversion', () => {
|
|
127
|
+
it('converts simple objects to YAML', () => {
|
|
128
|
+
const context = { topic: 'TypeScript', level: 'beginner' }
|
|
129
|
+
const result = parseTemplate`Write about ${{ context }}`
|
|
130
|
+
|
|
131
|
+
expect(result).toContain('context:')
|
|
132
|
+
expect(result).toContain('topic: TypeScript')
|
|
133
|
+
expect(result).toContain('level: beginner')
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('converts arrays to YAML lists', () => {
|
|
137
|
+
const topics = ['React', 'Vue', 'Angular']
|
|
138
|
+
const result = parseTemplate`Compare ${topics}`
|
|
139
|
+
|
|
140
|
+
expect(result).toContain('- React')
|
|
141
|
+
expect(result).toContain('- Vue')
|
|
142
|
+
expect(result).toContain('- Angular')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('handles nested objects', () => {
|
|
146
|
+
const brand = {
|
|
147
|
+
hero: 'developers',
|
|
148
|
+
problem: {
|
|
149
|
+
internal: 'complexity',
|
|
150
|
+
external: 'time constraints',
|
|
151
|
+
},
|
|
152
|
+
}
|
|
153
|
+
const result = parseTemplate`Create a story for ${{ brand }}`
|
|
154
|
+
|
|
155
|
+
expect(result).toContain('brand:')
|
|
156
|
+
expect(result).toContain('hero: developers')
|
|
157
|
+
expect(result).toContain('problem:')
|
|
158
|
+
expect(result).toContain('internal: complexity')
|
|
159
|
+
expect(result).toContain('external: time constraints')
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('handles arrays of objects', () => {
|
|
163
|
+
const users = [
|
|
164
|
+
{ name: 'Alice', role: 'admin' },
|
|
165
|
+
{ name: 'Bob', role: 'user' },
|
|
166
|
+
]
|
|
167
|
+
const result = parseTemplate`Process users: ${users}`
|
|
168
|
+
|
|
169
|
+
expect(result).toContain('- name: Alice')
|
|
170
|
+
expect(result).toContain('role: admin')
|
|
171
|
+
expect(result).toContain('- name: Bob')
|
|
172
|
+
expect(result).toContain('role: user')
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('handles empty objects', () => {
|
|
176
|
+
const empty = {}
|
|
177
|
+
const result = parseTemplate`Config: ${empty}`
|
|
178
|
+
expect(result).toContain('Config:')
|
|
179
|
+
expect(result).toContain('{}')
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('handles empty arrays', () => {
|
|
183
|
+
const empty: string[] = []
|
|
184
|
+
const result = parseTemplate`Items: ${empty}`
|
|
185
|
+
expect(result).toContain('Items:')
|
|
186
|
+
expect(result).toContain('[]')
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('handles deeply nested structures', () => {
|
|
190
|
+
const data = {
|
|
191
|
+
level1: {
|
|
192
|
+
level2: {
|
|
193
|
+
level3: {
|
|
194
|
+
value: 'deep',
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
}
|
|
199
|
+
const result = parseTemplate`Data: ${data}`
|
|
200
|
+
|
|
201
|
+
expect(result).toContain('level1:')
|
|
202
|
+
expect(result).toContain('level2:')
|
|
203
|
+
expect(result).toContain('level3:')
|
|
204
|
+
expect(result).toContain('value: deep')
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('adds newline before YAML content', () => {
|
|
208
|
+
const obj = { key: 'value' }
|
|
209
|
+
const result = parseTemplate`Config:${obj}`
|
|
210
|
+
|
|
211
|
+
// Should have newline between text and YAML
|
|
212
|
+
expect(result).toMatch(/Config:\n/)
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
describe('mixed content', () => {
|
|
217
|
+
it('handles mix of primitives and objects', () => {
|
|
218
|
+
const count = 5
|
|
219
|
+
const options = { format: 'json', verbose: true }
|
|
220
|
+
const result = parseTemplate`Generate ${count} items with ${options}`
|
|
221
|
+
|
|
222
|
+
expect(result).toContain('Generate 5 items with')
|
|
223
|
+
expect(result).toContain('format: json')
|
|
224
|
+
expect(result).toContain('verbose: true')
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('preserves template structure with objects inline', () => {
|
|
228
|
+
const requirements = {
|
|
229
|
+
pages: ['home', 'about', 'contact'],
|
|
230
|
+
features: ['dark mode', 'responsive'],
|
|
231
|
+
}
|
|
232
|
+
const result = parseTemplate`marketing site${{ requirements }}`
|
|
233
|
+
|
|
234
|
+
expect(result).toContain('marketing site')
|
|
235
|
+
expect(result).toContain('requirements:')
|
|
236
|
+
expect(result).toContain('pages:')
|
|
237
|
+
expect(result).toContain('- home')
|
|
238
|
+
expect(result).toContain('features:')
|
|
239
|
+
expect(result).toContain('- dark mode')
|
|
240
|
+
})
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
describe('special values', () => {
|
|
244
|
+
it('handles Date objects', () => {
|
|
245
|
+
const date = new Date('2024-01-15T10:30:00Z')
|
|
246
|
+
const result = parseTemplate`Date: ${date}`
|
|
247
|
+
// Date objects are converted via YAML
|
|
248
|
+
expect(result).toContain('Date:')
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('handles RegExp objects', () => {
|
|
252
|
+
const regex = /test\d+/gi
|
|
253
|
+
const result = parseTemplate`Pattern: ${regex}`
|
|
254
|
+
expect(result).toContain('Pattern:')
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('handles functions (converts to string)', () => {
|
|
258
|
+
const fn = () => 'test'
|
|
259
|
+
// Functions are not objects in typeof sense for this check
|
|
260
|
+
// Actually typeof function is 'function', not 'object'
|
|
261
|
+
const result = parseTemplate`Function: ${fn}`
|
|
262
|
+
expect(result).toContain('Function:')
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('handles Symbol', () => {
|
|
266
|
+
const sym = Symbol('test')
|
|
267
|
+
const result = parseTemplate`Symbol: ${sym}`
|
|
268
|
+
expect(result).toContain('Symbol(test)')
|
|
269
|
+
})
|
|
270
|
+
})
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
// ============================================================================
|
|
274
|
+
// createChainablePromise Tests
|
|
275
|
+
// ============================================================================
|
|
276
|
+
|
|
277
|
+
describe('createChainablePromise', () => {
|
|
278
|
+
describe('basic promise behavior', () => {
|
|
279
|
+
it('can be awaited directly', async () => {
|
|
280
|
+
const executor = vi.fn().mockResolvedValue('result')
|
|
281
|
+
const chainable = createChainablePromise(executor)
|
|
282
|
+
|
|
283
|
+
const result = await chainable
|
|
284
|
+
expect(result).toBe('result')
|
|
285
|
+
expect(executor).toHaveBeenCalledTimes(1)
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it('supports .then()', async () => {
|
|
289
|
+
const executor = vi.fn().mockResolvedValue('result')
|
|
290
|
+
const chainable = createChainablePromise(executor)
|
|
291
|
+
|
|
292
|
+
const result = await chainable.then((v) => v.toUpperCase())
|
|
293
|
+
expect(result).toBe('RESULT')
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
it('supports .catch()', async () => {
|
|
297
|
+
const executor = vi.fn().mockRejectedValue(new Error('test error'))
|
|
298
|
+
const chainable = createChainablePromise(executor)
|
|
299
|
+
|
|
300
|
+
const error = await chainable.catch((e) => e.message)
|
|
301
|
+
expect(error).toBe('test error')
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
it('supports .finally()', async () => {
|
|
305
|
+
const executor = vi.fn().mockResolvedValue('result')
|
|
306
|
+
const finallyFn = vi.fn()
|
|
307
|
+
const chainable = createChainablePromise(executor)
|
|
308
|
+
|
|
309
|
+
await chainable.finally(finallyFn)
|
|
310
|
+
expect(finallyFn).toHaveBeenCalled()
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
it('supports chained .then().catch().finally()', async () => {
|
|
314
|
+
const executor = vi.fn().mockResolvedValue(5)
|
|
315
|
+
const finallyFn = vi.fn()
|
|
316
|
+
const chainable = createChainablePromise(executor)
|
|
317
|
+
|
|
318
|
+
const result = await chainable
|
|
319
|
+
.then((v) => v * 2)
|
|
320
|
+
.catch(() => 0)
|
|
321
|
+
.finally(finallyFn)
|
|
322
|
+
|
|
323
|
+
expect(result).toBe(10)
|
|
324
|
+
expect(finallyFn).toHaveBeenCalled()
|
|
325
|
+
})
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
describe('options chaining', () => {
|
|
329
|
+
it('can be called with options', async () => {
|
|
330
|
+
const executor = vi.fn().mockImplementation((opts) => Promise.resolve(opts))
|
|
331
|
+
const chainable = createChainablePromise(executor)
|
|
332
|
+
|
|
333
|
+
const result = await chainable({ model: 'claude-opus-4-5' })
|
|
334
|
+
expect(result).toEqual({ model: 'claude-opus-4-5' })
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
it('merges options with default options', async () => {
|
|
338
|
+
const executor = vi.fn().mockImplementation((opts) => Promise.resolve(opts))
|
|
339
|
+
const defaultOptions: FunctionOptions = { model: 'sonnet', temperature: 0.5 }
|
|
340
|
+
const chainable = createChainablePromise(executor, defaultOptions)
|
|
341
|
+
|
|
342
|
+
const result = await chainable({ temperature: 0.9, maxTokens: 1000 })
|
|
343
|
+
expect(result).toEqual({
|
|
344
|
+
model: 'sonnet',
|
|
345
|
+
temperature: 0.9,
|
|
346
|
+
maxTokens: 1000,
|
|
347
|
+
})
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
it('overrides default options when called with same option', async () => {
|
|
351
|
+
const executor = vi.fn().mockImplementation((opts) => Promise.resolve(opts))
|
|
352
|
+
const defaultOptions: FunctionOptions = { model: 'sonnet' }
|
|
353
|
+
const chainable = createChainablePromise(executor, defaultOptions)
|
|
354
|
+
|
|
355
|
+
const result = await chainable({ model: 'claude-opus-4-5' })
|
|
356
|
+
expect(result).toEqual({ model: 'claude-opus-4-5' })
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
it('uses default options when awaited directly', async () => {
|
|
360
|
+
const executor = vi.fn().mockImplementation((opts) => Promise.resolve(opts))
|
|
361
|
+
const defaultOptions: FunctionOptions = { model: 'sonnet' }
|
|
362
|
+
const chainable = createChainablePromise(executor, defaultOptions)
|
|
363
|
+
|
|
364
|
+
const result = await chainable
|
|
365
|
+
expect(result).toEqual({ model: 'sonnet' })
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
it('handles undefined options call', async () => {
|
|
369
|
+
const executor = vi.fn().mockImplementation((opts) => Promise.resolve(opts))
|
|
370
|
+
const chainable = createChainablePromise(executor)
|
|
371
|
+
|
|
372
|
+
// Calling with undefined merges {...defaultOptions, ...undefined} = {}
|
|
373
|
+
const result = await chainable(undefined)
|
|
374
|
+
expect(result).toEqual({})
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
it('handles empty options object', async () => {
|
|
378
|
+
const executor = vi.fn().mockImplementation((opts) => Promise.resolve(opts))
|
|
379
|
+
const defaultOptions: FunctionOptions = { model: 'sonnet' }
|
|
380
|
+
const chainable = createChainablePromise(executor, defaultOptions)
|
|
381
|
+
|
|
382
|
+
const result = await chainable({})
|
|
383
|
+
expect(result).toEqual({ model: 'sonnet' })
|
|
384
|
+
})
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
describe('executor behavior', () => {
|
|
388
|
+
it('calls executor with default options on creation', () => {
|
|
389
|
+
const executor = vi.fn().mockResolvedValue('result')
|
|
390
|
+
const defaultOptions: FunctionOptions = { model: 'sonnet' }
|
|
391
|
+
createChainablePromise(executor, defaultOptions)
|
|
392
|
+
|
|
393
|
+
expect(executor).toHaveBeenCalledWith(defaultOptions)
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
it('creates new promise when called with options', async () => {
|
|
397
|
+
const executor = vi.fn().mockResolvedValue('result')
|
|
398
|
+
const chainable = createChainablePromise(executor)
|
|
399
|
+
|
|
400
|
+
// First call happens on creation
|
|
401
|
+
expect(executor).toHaveBeenCalledTimes(1)
|
|
402
|
+
|
|
403
|
+
// Calling with options creates a new promise
|
|
404
|
+
await chainable({ model: 'claude-opus-4-5' })
|
|
405
|
+
expect(executor).toHaveBeenCalledTimes(2)
|
|
406
|
+
})
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
describe('type behavior', () => {
|
|
410
|
+
it('preserves generic type through await', async () => {
|
|
411
|
+
interface User {
|
|
412
|
+
name: string
|
|
413
|
+
age: number
|
|
414
|
+
}
|
|
415
|
+
const user: User = { name: 'Alice', age: 30 }
|
|
416
|
+
const executor = vi.fn().mockResolvedValue(user)
|
|
417
|
+
const chainable = createChainablePromise<User>(executor)
|
|
418
|
+
|
|
419
|
+
const result = await chainable
|
|
420
|
+
expect(result.name).toBe('Alice')
|
|
421
|
+
expect(result.age).toBe(30)
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
it('preserves generic type through options call', async () => {
|
|
425
|
+
interface User {
|
|
426
|
+
name: string
|
|
427
|
+
}
|
|
428
|
+
const user: User = { name: 'Bob' }
|
|
429
|
+
const executor = vi.fn().mockResolvedValue(user)
|
|
430
|
+
const chainable = createChainablePromise<User>(executor)
|
|
431
|
+
|
|
432
|
+
const result = await chainable({ model: 'sonnet' })
|
|
433
|
+
expect(result.name).toBe('Bob')
|
|
434
|
+
})
|
|
435
|
+
})
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
// ============================================================================
|
|
439
|
+
// createTemplateFunction Tests
|
|
440
|
+
// ============================================================================
|
|
441
|
+
|
|
442
|
+
describe('createTemplateFunction', () => {
|
|
443
|
+
describe('tagged template literal syntax', () => {
|
|
444
|
+
it('handles tagged template with simple interpolation', async () => {
|
|
445
|
+
let capturedPrompt = ''
|
|
446
|
+
const handler = async (prompt: string) => {
|
|
447
|
+
capturedPrompt = prompt
|
|
448
|
+
return 'result'
|
|
449
|
+
}
|
|
450
|
+
const fn = createTemplateFunction(handler)
|
|
451
|
+
|
|
452
|
+
const result = await fn`Hello ${'world'}`
|
|
453
|
+
expect(capturedPrompt).toBe('Hello world')
|
|
454
|
+
expect(result).toBe('result')
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
it('handles tagged template with objects', async () => {
|
|
458
|
+
let capturedPrompt = ''
|
|
459
|
+
const handler = async (prompt: string) => {
|
|
460
|
+
capturedPrompt = prompt
|
|
461
|
+
return 'result'
|
|
462
|
+
}
|
|
463
|
+
const fn = createTemplateFunction(handler)
|
|
464
|
+
|
|
465
|
+
const data = { name: 'test', value: 42 }
|
|
466
|
+
await fn`Process ${data}`
|
|
467
|
+
|
|
468
|
+
expect(capturedPrompt).toContain('name: test')
|
|
469
|
+
expect(capturedPrompt).toContain('value: 42')
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
it('handles tagged template without interpolations', async () => {
|
|
473
|
+
let capturedPrompt = ''
|
|
474
|
+
const handler = async (prompt: string) => {
|
|
475
|
+
capturedPrompt = prompt
|
|
476
|
+
return 'result'
|
|
477
|
+
}
|
|
478
|
+
const fn = createTemplateFunction(handler)
|
|
479
|
+
|
|
480
|
+
await fn`Plain prompt`
|
|
481
|
+
expect(capturedPrompt).toBe('Plain prompt')
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
it('returns chainable promise from tagged template', async () => {
|
|
485
|
+
const handler = async (prompt: string, options?: FunctionOptions) =>
|
|
486
|
+
options?.model ?? 'default'
|
|
487
|
+
const fn = createTemplateFunction(handler)
|
|
488
|
+
|
|
489
|
+
// Should be chainable
|
|
490
|
+
const result = await fn`test`({ model: 'claude-opus-4-5' })
|
|
491
|
+
expect(result).toBe('claude-opus-4-5')
|
|
492
|
+
})
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
describe('regular function call syntax', () => {
|
|
496
|
+
it('handles regular string call', async () => {
|
|
497
|
+
let capturedPrompt = ''
|
|
498
|
+
const handler = async (prompt: string) => {
|
|
499
|
+
capturedPrompt = prompt
|
|
500
|
+
return 'result'
|
|
501
|
+
}
|
|
502
|
+
const fn = createTemplateFunction(handler)
|
|
503
|
+
|
|
504
|
+
await fn('Hello world')
|
|
505
|
+
expect(capturedPrompt).toBe('Hello world')
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
it('handles regular call with options', async () => {
|
|
509
|
+
let capturedOptions: FunctionOptions | undefined
|
|
510
|
+
const handler = async (prompt: string, options?: FunctionOptions) => {
|
|
511
|
+
capturedOptions = options
|
|
512
|
+
return 'result'
|
|
513
|
+
}
|
|
514
|
+
const fn = createTemplateFunction(handler)
|
|
515
|
+
|
|
516
|
+
await fn('Hello world', { model: 'claude-opus-4-5', temperature: 0.7 })
|
|
517
|
+
expect(capturedOptions).toEqual({ model: 'claude-opus-4-5', temperature: 0.7 })
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
it('returns regular promise from string call', async () => {
|
|
521
|
+
const handler = async () => 'result'
|
|
522
|
+
const fn = createTemplateFunction(handler)
|
|
523
|
+
|
|
524
|
+
const promise = fn('test')
|
|
525
|
+
expect(promise).toBeInstanceOf(Promise)
|
|
526
|
+
expect(await promise).toBe('result')
|
|
527
|
+
})
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
describe('options chaining on tagged templates', () => {
|
|
531
|
+
it('supports model option', async () => {
|
|
532
|
+
let capturedOptions: FunctionOptions | undefined
|
|
533
|
+
const handler = async (_: string, options?: FunctionOptions) => {
|
|
534
|
+
capturedOptions = options
|
|
535
|
+
return 'ok'
|
|
536
|
+
}
|
|
537
|
+
const fn = createTemplateFunction(handler)
|
|
538
|
+
|
|
539
|
+
await fn`test`({ model: 'claude-opus-4-5' })
|
|
540
|
+
expect(capturedOptions?.model).toBe('claude-opus-4-5')
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
it('supports thinking option', async () => {
|
|
544
|
+
let capturedOptions: FunctionOptions | undefined
|
|
545
|
+
const handler = async (_: string, options?: FunctionOptions) => {
|
|
546
|
+
capturedOptions = options
|
|
547
|
+
return 'ok'
|
|
548
|
+
}
|
|
549
|
+
const fn = createTemplateFunction(handler)
|
|
550
|
+
|
|
551
|
+
await fn`analysis`({ thinking: 'high' })
|
|
552
|
+
expect(capturedOptions?.thinking).toBe('high')
|
|
553
|
+
})
|
|
554
|
+
|
|
555
|
+
it('supports thinking as token budget', async () => {
|
|
556
|
+
let capturedOptions: FunctionOptions | undefined
|
|
557
|
+
const handler = async (_: string, options?: FunctionOptions) => {
|
|
558
|
+
capturedOptions = options
|
|
559
|
+
return 'ok'
|
|
560
|
+
}
|
|
561
|
+
const fn = createTemplateFunction(handler)
|
|
562
|
+
|
|
563
|
+
await fn`analysis`({ thinking: 10000 })
|
|
564
|
+
expect(capturedOptions?.thinking).toBe(10000)
|
|
565
|
+
})
|
|
566
|
+
|
|
567
|
+
it('supports temperature option', async () => {
|
|
568
|
+
let capturedOptions: FunctionOptions | undefined
|
|
569
|
+
const handler = async (_: string, options?: FunctionOptions) => {
|
|
570
|
+
capturedOptions = options
|
|
571
|
+
return 'ok'
|
|
572
|
+
}
|
|
573
|
+
const fn = createTemplateFunction(handler)
|
|
574
|
+
|
|
575
|
+
await fn`creative`({ temperature: 0.9 })
|
|
576
|
+
expect(capturedOptions?.temperature).toBe(0.9)
|
|
577
|
+
})
|
|
578
|
+
|
|
579
|
+
it('supports maxTokens option', async () => {
|
|
580
|
+
let capturedOptions: FunctionOptions | undefined
|
|
581
|
+
const handler = async (_: string, options?: FunctionOptions) => {
|
|
582
|
+
capturedOptions = options
|
|
583
|
+
return 'ok'
|
|
584
|
+
}
|
|
585
|
+
const fn = createTemplateFunction(handler)
|
|
586
|
+
|
|
587
|
+
await fn`long article`({ maxTokens: 4000 })
|
|
588
|
+
expect(capturedOptions?.maxTokens).toBe(4000)
|
|
589
|
+
})
|
|
590
|
+
|
|
591
|
+
it('supports system option', async () => {
|
|
592
|
+
let capturedOptions: FunctionOptions | undefined
|
|
593
|
+
const handler = async (_: string, options?: FunctionOptions) => {
|
|
594
|
+
capturedOptions = options
|
|
595
|
+
return 'ok'
|
|
596
|
+
}
|
|
597
|
+
const fn = createTemplateFunction(handler)
|
|
598
|
+
|
|
599
|
+
await fn`task`({ system: 'You are helpful' })
|
|
600
|
+
expect(capturedOptions?.system).toBe('You are helpful')
|
|
601
|
+
})
|
|
602
|
+
|
|
603
|
+
it('supports mode option', async () => {
|
|
604
|
+
let capturedOptions: FunctionOptions | undefined
|
|
605
|
+
const handler = async (_: string, options?: FunctionOptions) => {
|
|
606
|
+
capturedOptions = options
|
|
607
|
+
return 'ok'
|
|
608
|
+
}
|
|
609
|
+
const fn = createTemplateFunction(handler)
|
|
610
|
+
|
|
611
|
+
await fn`task`({ mode: 'background' })
|
|
612
|
+
expect(capturedOptions?.mode).toBe('background')
|
|
613
|
+
})
|
|
614
|
+
|
|
615
|
+
it('supports multiple options together', async () => {
|
|
616
|
+
let capturedOptions: FunctionOptions | undefined
|
|
617
|
+
const handler = async (_: string, options?: FunctionOptions) => {
|
|
618
|
+
capturedOptions = options
|
|
619
|
+
return 'ok'
|
|
620
|
+
}
|
|
621
|
+
const fn = createTemplateFunction(handler)
|
|
622
|
+
|
|
623
|
+
await fn`complex task`({
|
|
624
|
+
model: 'claude-opus-4-5',
|
|
625
|
+
thinking: 'high',
|
|
626
|
+
temperature: 0.7,
|
|
627
|
+
maxTokens: 8000,
|
|
628
|
+
system: 'Be precise',
|
|
629
|
+
mode: 'background',
|
|
630
|
+
})
|
|
631
|
+
|
|
632
|
+
expect(capturedOptions).toEqual({
|
|
633
|
+
model: 'claude-opus-4-5',
|
|
634
|
+
thinking: 'high',
|
|
635
|
+
temperature: 0.7,
|
|
636
|
+
maxTokens: 8000,
|
|
637
|
+
system: 'Be precise',
|
|
638
|
+
mode: 'background',
|
|
639
|
+
})
|
|
640
|
+
})
|
|
641
|
+
})
|
|
642
|
+
|
|
643
|
+
describe('error handling', () => {
|
|
644
|
+
it('propagates errors from handler in tagged template', async () => {
|
|
645
|
+
const handler = async () => {
|
|
646
|
+
throw new Error('Handler error')
|
|
647
|
+
}
|
|
648
|
+
const fn = createTemplateFunction(handler)
|
|
649
|
+
|
|
650
|
+
await expect(fn`test`).rejects.toThrow('Handler error')
|
|
651
|
+
})
|
|
652
|
+
|
|
653
|
+
it('propagates errors from handler in regular call', async () => {
|
|
654
|
+
const handler = async () => {
|
|
655
|
+
throw new Error('Handler error')
|
|
656
|
+
}
|
|
657
|
+
const fn = createTemplateFunction(handler)
|
|
658
|
+
|
|
659
|
+
await expect(fn('test')).rejects.toThrow('Handler error')
|
|
660
|
+
})
|
|
661
|
+
|
|
662
|
+
it('propagates errors from options chaining', async () => {
|
|
663
|
+
const handler = async (_: string, opts?: FunctionOptions) => {
|
|
664
|
+
if (opts?.model === 'invalid') {
|
|
665
|
+
throw new Error('Invalid model')
|
|
666
|
+
}
|
|
667
|
+
return 'ok'
|
|
668
|
+
}
|
|
669
|
+
const fn = createTemplateFunction(handler)
|
|
670
|
+
|
|
671
|
+
await expect(fn`test`({ model: 'invalid' })).rejects.toThrow('Invalid model')
|
|
672
|
+
})
|
|
673
|
+
})
|
|
674
|
+
|
|
675
|
+
describe('type preservation', () => {
|
|
676
|
+
it('preserves return type', async () => {
|
|
677
|
+
interface Response {
|
|
678
|
+
text: string
|
|
679
|
+
tokens: number
|
|
680
|
+
}
|
|
681
|
+
const handler = async (): Promise<Response> => ({
|
|
682
|
+
text: 'hello',
|
|
683
|
+
tokens: 10,
|
|
684
|
+
})
|
|
685
|
+
const fn = createTemplateFunction(handler)
|
|
686
|
+
|
|
687
|
+
const result = await fn`test`
|
|
688
|
+
expect(result.text).toBe('hello')
|
|
689
|
+
expect(result.tokens).toBe(10)
|
|
690
|
+
})
|
|
691
|
+
})
|
|
692
|
+
})
|
|
693
|
+
|
|
694
|
+
// ============================================================================
|
|
695
|
+
// withBatch Tests
|
|
696
|
+
// ============================================================================
|
|
697
|
+
|
|
698
|
+
describe('withBatch', () => {
|
|
699
|
+
it('adds batch method to template function', () => {
|
|
700
|
+
const handler = async (prompt: string) => prompt.toUpperCase()
|
|
701
|
+
const fn = createTemplateFunction(handler)
|
|
702
|
+
const batchHandler = async (inputs: string[]) => inputs.map((i) => i.toUpperCase())
|
|
703
|
+
|
|
704
|
+
const batchable = withBatch(fn, batchHandler)
|
|
705
|
+
|
|
706
|
+
expect(typeof batchable.batch).toBe('function')
|
|
707
|
+
})
|
|
708
|
+
|
|
709
|
+
it('batch method processes array of inputs', async () => {
|
|
710
|
+
const handler = async (prompt: string) => prompt.toUpperCase()
|
|
711
|
+
const fn = createTemplateFunction(handler)
|
|
712
|
+
const batchHandler = async (inputs: string[]) => inputs.map((i) => i.toUpperCase())
|
|
713
|
+
|
|
714
|
+
const batchable = withBatch(fn, batchHandler)
|
|
715
|
+
const results = await batchable.batch(['hello', 'world'])
|
|
716
|
+
|
|
717
|
+
expect(results).toEqual(['HELLO', 'WORLD'])
|
|
718
|
+
})
|
|
719
|
+
|
|
720
|
+
it('preserves original template function behavior', async () => {
|
|
721
|
+
let capturedPrompt = ''
|
|
722
|
+
const handler = async (prompt: string) => {
|
|
723
|
+
capturedPrompt = prompt
|
|
724
|
+
return 'result'
|
|
725
|
+
}
|
|
726
|
+
const fn = createTemplateFunction(handler)
|
|
727
|
+
const batchHandler = async (inputs: string[]) => inputs
|
|
728
|
+
|
|
729
|
+
const batchable = withBatch(fn, batchHandler)
|
|
730
|
+
|
|
731
|
+
// Template syntax still works
|
|
732
|
+
await batchable`test ${'value'}`
|
|
733
|
+
expect(capturedPrompt).toBe('test value')
|
|
734
|
+
|
|
735
|
+
// Regular call still works
|
|
736
|
+
await batchable('direct call')
|
|
737
|
+
expect(capturedPrompt).toBe('direct call')
|
|
738
|
+
})
|
|
739
|
+
|
|
740
|
+
it('handles empty batch input', async () => {
|
|
741
|
+
const handler = async (prompt: string) => prompt
|
|
742
|
+
const fn = createTemplateFunction(handler)
|
|
743
|
+
const batchHandler = async (inputs: string[]) => inputs.map((i) => i.toUpperCase())
|
|
744
|
+
|
|
745
|
+
const batchable = withBatch(fn, batchHandler)
|
|
746
|
+
const results = await batchable.batch([])
|
|
747
|
+
|
|
748
|
+
expect(results).toEqual([])
|
|
749
|
+
})
|
|
750
|
+
|
|
751
|
+
it('supports typed batch inputs', async () => {
|
|
752
|
+
interface Task {
|
|
753
|
+
id: number
|
|
754
|
+
content: string
|
|
755
|
+
}
|
|
756
|
+
interface Result {
|
|
757
|
+
taskId: number
|
|
758
|
+
output: string
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const handler = async () => ({ taskId: 0, output: '' })
|
|
762
|
+
const fn = createTemplateFunction(handler)
|
|
763
|
+
const batchHandler = async (inputs: Task[]): Promise<Result[]> =>
|
|
764
|
+
inputs.map((t) => ({ taskId: t.id, output: t.content.toUpperCase() }))
|
|
765
|
+
|
|
766
|
+
const batchable = withBatch<Result, Task>(fn, batchHandler)
|
|
767
|
+
const results = await batchable.batch([
|
|
768
|
+
{ id: 1, content: 'hello' },
|
|
769
|
+
{ id: 2, content: 'world' },
|
|
770
|
+
])
|
|
771
|
+
|
|
772
|
+
expect(results).toEqual([
|
|
773
|
+
{ taskId: 1, output: 'HELLO' },
|
|
774
|
+
{ taskId: 2, output: 'WORLD' },
|
|
775
|
+
])
|
|
776
|
+
})
|
|
777
|
+
})
|
|
778
|
+
|
|
779
|
+
// ============================================================================
|
|
780
|
+
// createAsyncIterable Tests
|
|
781
|
+
// ============================================================================
|
|
782
|
+
|
|
783
|
+
describe('createAsyncIterable', () => {
|
|
784
|
+
describe('from array', () => {
|
|
785
|
+
it('creates async iterable from array', async () => {
|
|
786
|
+
const items = [1, 2, 3]
|
|
787
|
+
const iterable = createAsyncIterable(items)
|
|
788
|
+
|
|
789
|
+
const results: number[] = []
|
|
790
|
+
for await (const item of iterable) {
|
|
791
|
+
results.push(item)
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
expect(results).toEqual([1, 2, 3])
|
|
795
|
+
})
|
|
796
|
+
|
|
797
|
+
it('handles empty array', async () => {
|
|
798
|
+
const items: number[] = []
|
|
799
|
+
const iterable = createAsyncIterable(items)
|
|
800
|
+
|
|
801
|
+
const results: number[] = []
|
|
802
|
+
for await (const item of iterable) {
|
|
803
|
+
results.push(item)
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
expect(results).toEqual([])
|
|
807
|
+
})
|
|
808
|
+
|
|
809
|
+
it('handles array of objects', async () => {
|
|
810
|
+
const items = [{ id: 1 }, { id: 2 }]
|
|
811
|
+
const iterable = createAsyncIterable(items)
|
|
812
|
+
|
|
813
|
+
const results: { id: number }[] = []
|
|
814
|
+
for await (const item of iterable) {
|
|
815
|
+
results.push(item)
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
expect(results).toEqual([{ id: 1 }, { id: 2 }])
|
|
819
|
+
})
|
|
820
|
+
|
|
821
|
+
it('can be iterated multiple times (array source)', async () => {
|
|
822
|
+
const items = [1, 2, 3]
|
|
823
|
+
const iterable = createAsyncIterable(items)
|
|
824
|
+
|
|
825
|
+
const results1: number[] = []
|
|
826
|
+
for await (const item of iterable) {
|
|
827
|
+
results1.push(item)
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const results2: number[] = []
|
|
831
|
+
for await (const item of iterable) {
|
|
832
|
+
results2.push(item)
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
expect(results1).toEqual([1, 2, 3])
|
|
836
|
+
expect(results2).toEqual([1, 2, 3])
|
|
837
|
+
})
|
|
838
|
+
})
|
|
839
|
+
|
|
840
|
+
describe('from generator', () => {
|
|
841
|
+
it('creates async iterable from generator function', async () => {
|
|
842
|
+
async function* generator() {
|
|
843
|
+
yield 1
|
|
844
|
+
yield 2
|
|
845
|
+
yield 3
|
|
846
|
+
}
|
|
847
|
+
const iterable = createAsyncIterable(generator)
|
|
848
|
+
|
|
849
|
+
const results: number[] = []
|
|
850
|
+
for await (const item of iterable) {
|
|
851
|
+
results.push(item)
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
expect(results).toEqual([1, 2, 3])
|
|
855
|
+
})
|
|
856
|
+
|
|
857
|
+
it('handles generator that yields nothing', async () => {
|
|
858
|
+
async function* generator() {
|
|
859
|
+
// yields nothing
|
|
860
|
+
}
|
|
861
|
+
const iterable = createAsyncIterable(generator)
|
|
862
|
+
|
|
863
|
+
const results: number[] = []
|
|
864
|
+
for await (const item of iterable) {
|
|
865
|
+
results.push(item)
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
expect(results).toEqual([])
|
|
869
|
+
})
|
|
870
|
+
|
|
871
|
+
it('handles generator with delays', async () => {
|
|
872
|
+
async function* generator() {
|
|
873
|
+
yield 1
|
|
874
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
875
|
+
yield 2
|
|
876
|
+
}
|
|
877
|
+
const iterable = createAsyncIterable(generator)
|
|
878
|
+
|
|
879
|
+
const results: number[] = []
|
|
880
|
+
for await (const item of iterable) {
|
|
881
|
+
results.push(item)
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
expect(results).toEqual([1, 2])
|
|
885
|
+
})
|
|
886
|
+
|
|
887
|
+
it('propagates generator errors', async () => {
|
|
888
|
+
async function* generator() {
|
|
889
|
+
yield 1
|
|
890
|
+
throw new Error('Generator error')
|
|
891
|
+
}
|
|
892
|
+
const iterable = createAsyncIterable(generator)
|
|
893
|
+
|
|
894
|
+
const results: number[] = []
|
|
895
|
+
await expect(async () => {
|
|
896
|
+
for await (const item of iterable) {
|
|
897
|
+
results.push(item)
|
|
898
|
+
}
|
|
899
|
+
}).rejects.toThrow('Generator error')
|
|
900
|
+
|
|
901
|
+
expect(results).toEqual([1])
|
|
902
|
+
})
|
|
903
|
+
})
|
|
904
|
+
})
|
|
905
|
+
|
|
906
|
+
// ============================================================================
|
|
907
|
+
// createStreamableList Tests
|
|
908
|
+
// ============================================================================
|
|
909
|
+
|
|
910
|
+
describe('createStreamableList', () => {
|
|
911
|
+
describe('promise behavior', () => {
|
|
912
|
+
it('can be awaited to get full array', async () => {
|
|
913
|
+
const getItems = async () => [1, 2, 3]
|
|
914
|
+
const streamable = createStreamableList(getItems)
|
|
915
|
+
|
|
916
|
+
const result = await streamable
|
|
917
|
+
expect(result).toEqual([1, 2, 3])
|
|
918
|
+
})
|
|
919
|
+
|
|
920
|
+
it('supports .then()', async () => {
|
|
921
|
+
const getItems = async () => [1, 2, 3]
|
|
922
|
+
const streamable = createStreamableList(getItems)
|
|
923
|
+
|
|
924
|
+
const result = await streamable.then((items) => items.length)
|
|
925
|
+
expect(result).toBe(3)
|
|
926
|
+
})
|
|
927
|
+
|
|
928
|
+
it('supports .catch()', async () => {
|
|
929
|
+
const getItems = async () => {
|
|
930
|
+
throw new Error('Get items error')
|
|
931
|
+
}
|
|
932
|
+
const streamable = createStreamableList<number>(getItems)
|
|
933
|
+
|
|
934
|
+
const error = await streamable.catch((e) => e.message)
|
|
935
|
+
expect(error).toBe('Get items error')
|
|
936
|
+
})
|
|
937
|
+
|
|
938
|
+
it('supports .finally()', async () => {
|
|
939
|
+
const getItems = async () => [1, 2, 3]
|
|
940
|
+
const finallyFn = vi.fn()
|
|
941
|
+
const streamable = createStreamableList(getItems)
|
|
942
|
+
|
|
943
|
+
await streamable.finally(finallyFn)
|
|
944
|
+
expect(finallyFn).toHaveBeenCalled()
|
|
945
|
+
})
|
|
946
|
+
|
|
947
|
+
it('has Promise Symbol.toStringTag', () => {
|
|
948
|
+
const getItems = async () => [1, 2, 3]
|
|
949
|
+
const streamable = createStreamableList(getItems)
|
|
950
|
+
|
|
951
|
+
expect(Object.prototype.toString.call(streamable)).toBe('[object Promise]')
|
|
952
|
+
})
|
|
953
|
+
})
|
|
954
|
+
|
|
955
|
+
describe('async iteration without custom stream', () => {
|
|
956
|
+
it('iterates over resolved items when no streamItems provided', async () => {
|
|
957
|
+
const getItems = async () => [1, 2, 3]
|
|
958
|
+
const streamable = createStreamableList(getItems)
|
|
959
|
+
|
|
960
|
+
const results: number[] = []
|
|
961
|
+
for await (const item of streamable) {
|
|
962
|
+
results.push(item)
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
expect(results).toEqual([1, 2, 3])
|
|
966
|
+
})
|
|
967
|
+
|
|
968
|
+
it('handles empty array', async () => {
|
|
969
|
+
const getItems = async () => [] as number[]
|
|
970
|
+
const streamable = createStreamableList(getItems)
|
|
971
|
+
|
|
972
|
+
const results: number[] = []
|
|
973
|
+
for await (const item of streamable) {
|
|
974
|
+
results.push(item)
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
expect(results).toEqual([])
|
|
978
|
+
})
|
|
979
|
+
})
|
|
980
|
+
|
|
981
|
+
describe('async iteration with custom stream', () => {
|
|
982
|
+
it('uses custom streamItems generator when provided', async () => {
|
|
983
|
+
const getItems = async () => [1, 2, 3]
|
|
984
|
+
async function* streamItems() {
|
|
985
|
+
yield 10
|
|
986
|
+
yield 20
|
|
987
|
+
yield 30
|
|
988
|
+
}
|
|
989
|
+
const streamable = createStreamableList(getItems, streamItems)
|
|
990
|
+
|
|
991
|
+
const results: number[] = []
|
|
992
|
+
for await (const item of streamable) {
|
|
993
|
+
results.push(item)
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// Should use the stream, not the array
|
|
997
|
+
expect(results).toEqual([10, 20, 30])
|
|
998
|
+
})
|
|
999
|
+
|
|
1000
|
+
it('stream can yield different items than getItems returns', async () => {
|
|
1001
|
+
// This simulates streaming where you might get incremental items
|
|
1002
|
+
const getItems = async () => ['final1', 'final2']
|
|
1003
|
+
async function* streamItems() {
|
|
1004
|
+
yield 'stream1'
|
|
1005
|
+
yield 'stream2'
|
|
1006
|
+
yield 'stream3'
|
|
1007
|
+
}
|
|
1008
|
+
const streamable = createStreamableList(getItems, streamItems)
|
|
1009
|
+
|
|
1010
|
+
// Await gets the promise result
|
|
1011
|
+
const awaited = await streamable
|
|
1012
|
+
|
|
1013
|
+
// But iterating uses the stream
|
|
1014
|
+
const streamed: string[] = []
|
|
1015
|
+
const freshStreamable = createStreamableList(getItems, streamItems)
|
|
1016
|
+
for await (const item of freshStreamable) {
|
|
1017
|
+
streamed.push(item)
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
expect(awaited).toEqual(['final1', 'final2'])
|
|
1021
|
+
expect(streamed).toEqual(['stream1', 'stream2', 'stream3'])
|
|
1022
|
+
})
|
|
1023
|
+
|
|
1024
|
+
it('handles stream errors separately from promise', async () => {
|
|
1025
|
+
const getItems = async () => [1, 2, 3]
|
|
1026
|
+
async function* streamItems() {
|
|
1027
|
+
yield 1
|
|
1028
|
+
throw new Error('Stream error')
|
|
1029
|
+
}
|
|
1030
|
+
const streamable = createStreamableList(getItems, streamItems)
|
|
1031
|
+
|
|
1032
|
+
// Promise should resolve fine
|
|
1033
|
+
const awaited = await streamable
|
|
1034
|
+
expect(awaited).toEqual([1, 2, 3])
|
|
1035
|
+
|
|
1036
|
+
// But streaming should error
|
|
1037
|
+
const freshStreamable = createStreamableList(getItems, streamItems)
|
|
1038
|
+
await expect(async () => {
|
|
1039
|
+
for await (const _item of freshStreamable) {
|
|
1040
|
+
// iterate
|
|
1041
|
+
}
|
|
1042
|
+
}).rejects.toThrow('Stream error')
|
|
1043
|
+
})
|
|
1044
|
+
})
|
|
1045
|
+
|
|
1046
|
+
describe('dual usage patterns', () => {
|
|
1047
|
+
it('can await and iterate on same streamable (with default stream)', async () => {
|
|
1048
|
+
let callCount = 0
|
|
1049
|
+
const getItems = async () => {
|
|
1050
|
+
callCount++
|
|
1051
|
+
return [1, 2, 3]
|
|
1052
|
+
}
|
|
1053
|
+
const streamable = createStreamableList(getItems)
|
|
1054
|
+
|
|
1055
|
+
// Await first
|
|
1056
|
+
const awaited = await streamable
|
|
1057
|
+
expect(awaited).toEqual([1, 2, 3])
|
|
1058
|
+
|
|
1059
|
+
// Then iterate (uses same resolved promise)
|
|
1060
|
+
const iterated: number[] = []
|
|
1061
|
+
for await (const item of streamable) {
|
|
1062
|
+
iterated.push(item)
|
|
1063
|
+
}
|
|
1064
|
+
expect(iterated).toEqual([1, 2, 3])
|
|
1065
|
+
|
|
1066
|
+
// getItems should only be called once
|
|
1067
|
+
expect(callCount).toBe(1)
|
|
1068
|
+
})
|
|
1069
|
+
|
|
1070
|
+
it('works with Promise.all', async () => {
|
|
1071
|
+
const streamable1 = createStreamableList(async () => [1, 2])
|
|
1072
|
+
const streamable2 = createStreamableList(async () => [3, 4])
|
|
1073
|
+
|
|
1074
|
+
const [result1, result2] = await Promise.all([streamable1, streamable2])
|
|
1075
|
+
|
|
1076
|
+
expect(result1).toEqual([1, 2])
|
|
1077
|
+
expect(result2).toEqual([3, 4])
|
|
1078
|
+
})
|
|
1079
|
+
|
|
1080
|
+
it('works with Promise.race', async () => {
|
|
1081
|
+
const slow = createStreamableList(async () => {
|
|
1082
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
1083
|
+
return ['slow']
|
|
1084
|
+
})
|
|
1085
|
+
const fast = createStreamableList(async () => ['fast'])
|
|
1086
|
+
|
|
1087
|
+
const result = await Promise.race([slow, fast])
|
|
1088
|
+
expect(result).toEqual(['fast'])
|
|
1089
|
+
})
|
|
1090
|
+
})
|
|
1091
|
+
|
|
1092
|
+
describe('type preservation', () => {
|
|
1093
|
+
it('preserves generic type through await', async () => {
|
|
1094
|
+
interface Item {
|
|
1095
|
+
id: number
|
|
1096
|
+
name: string
|
|
1097
|
+
}
|
|
1098
|
+
const getItems = async (): Promise<Item[]> => [
|
|
1099
|
+
{ id: 1, name: 'one' },
|
|
1100
|
+
{ id: 2, name: 'two' },
|
|
1101
|
+
]
|
|
1102
|
+
const streamable = createStreamableList(getItems)
|
|
1103
|
+
|
|
1104
|
+
const result = await streamable
|
|
1105
|
+
expect(result[0].id).toBe(1)
|
|
1106
|
+
expect(result[0].name).toBe('one')
|
|
1107
|
+
})
|
|
1108
|
+
|
|
1109
|
+
it('preserves generic type through iteration', async () => {
|
|
1110
|
+
interface Item {
|
|
1111
|
+
id: number
|
|
1112
|
+
}
|
|
1113
|
+
const getItems = async (): Promise<Item[]> => [{ id: 1 }, { id: 2 }]
|
|
1114
|
+
const streamable = createStreamableList(getItems)
|
|
1115
|
+
|
|
1116
|
+
for await (const item of streamable) {
|
|
1117
|
+
expect(typeof item.id).toBe('number')
|
|
1118
|
+
}
|
|
1119
|
+
})
|
|
1120
|
+
})
|
|
1121
|
+
})
|
|
1122
|
+
|
|
1123
|
+
// ============================================================================
|
|
1124
|
+
// Type Export Tests
|
|
1125
|
+
// ============================================================================
|
|
1126
|
+
|
|
1127
|
+
describe('type exports', () => {
|
|
1128
|
+
it('exports FunctionOptions type', () => {
|
|
1129
|
+
const opts: FunctionOptions = {
|
|
1130
|
+
model: 'claude-opus-4-5',
|
|
1131
|
+
thinking: 'high',
|
|
1132
|
+
temperature: 0.7,
|
|
1133
|
+
maxTokens: 1000,
|
|
1134
|
+
system: 'Be helpful',
|
|
1135
|
+
mode: 'background',
|
|
1136
|
+
}
|
|
1137
|
+
expect(opts.model).toBe('claude-opus-4-5')
|
|
1138
|
+
})
|
|
1139
|
+
|
|
1140
|
+
it('exports ChainablePromise type', () => {
|
|
1141
|
+
// Type check - this should compile
|
|
1142
|
+
const _check: ChainablePromise<string> = createChainablePromise(async () => 'test')
|
|
1143
|
+
expect(_check).toBeDefined()
|
|
1144
|
+
})
|
|
1145
|
+
|
|
1146
|
+
it('exports TemplateFunction type', () => {
|
|
1147
|
+
// Type check - this should compile
|
|
1148
|
+
const _check: TemplateFunction<string> = createTemplateFunction(async () => 'test')
|
|
1149
|
+
expect(_check).toBeDefined()
|
|
1150
|
+
})
|
|
1151
|
+
|
|
1152
|
+
it('exports BatchableFunction type', () => {
|
|
1153
|
+
const fn = createTemplateFunction(async () => 'test')
|
|
1154
|
+
const _check: BatchableFunction<string> = withBatch(fn, async (inputs) =>
|
|
1155
|
+
inputs.map(() => 'test')
|
|
1156
|
+
)
|
|
1157
|
+
expect(_check.batch).toBeDefined()
|
|
1158
|
+
})
|
|
1159
|
+
|
|
1160
|
+
it('exports StreamableList type', () => {
|
|
1161
|
+
const _check: StreamableList<number> = createStreamableList(async () => [1, 2, 3])
|
|
1162
|
+
expect(_check).toBeDefined()
|
|
1163
|
+
})
|
|
1164
|
+
})
|