ai-functions 0.2.19 → 0.4.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.
Files changed (227) hide show
  1. package/.turbo/turbo-build.log +5 -0
  2. package/.turbo/turbo-test.log +105 -0
  3. package/README.md +232 -37
  4. package/TODO.md +138 -0
  5. package/dist/ai-promise.d.ts +219 -0
  6. package/dist/ai-promise.d.ts.map +1 -0
  7. package/dist/ai-promise.js +610 -0
  8. package/dist/ai-promise.js.map +1 -0
  9. package/dist/ai.d.ts +285 -0
  10. package/dist/ai.d.ts.map +1 -0
  11. package/dist/ai.js +842 -0
  12. package/dist/ai.js.map +1 -0
  13. package/dist/batch/anthropic.d.ts +23 -0
  14. package/dist/batch/anthropic.d.ts.map +1 -0
  15. package/dist/batch/anthropic.js +257 -0
  16. package/dist/batch/anthropic.js.map +1 -0
  17. package/dist/batch/bedrock.d.ts +64 -0
  18. package/dist/batch/bedrock.d.ts.map +1 -0
  19. package/dist/batch/bedrock.js +586 -0
  20. package/dist/batch/bedrock.js.map +1 -0
  21. package/dist/batch/cloudflare.d.ts +37 -0
  22. package/dist/batch/cloudflare.d.ts.map +1 -0
  23. package/dist/batch/cloudflare.js +289 -0
  24. package/dist/batch/cloudflare.js.map +1 -0
  25. package/dist/batch/google.d.ts +41 -0
  26. package/dist/batch/google.d.ts.map +1 -0
  27. package/dist/batch/google.js +360 -0
  28. package/dist/batch/google.js.map +1 -0
  29. package/dist/batch/index.d.ts +31 -0
  30. package/dist/batch/index.d.ts.map +1 -0
  31. package/dist/batch/index.js +31 -0
  32. package/dist/batch/index.js.map +1 -0
  33. package/dist/batch/memory.d.ts +44 -0
  34. package/dist/batch/memory.d.ts.map +1 -0
  35. package/dist/batch/memory.js +188 -0
  36. package/dist/batch/memory.js.map +1 -0
  37. package/dist/batch/openai.d.ts +37 -0
  38. package/dist/batch/openai.d.ts.map +1 -0
  39. package/dist/batch/openai.js +403 -0
  40. package/dist/batch/openai.js.map +1 -0
  41. package/dist/batch-map.d.ts +125 -0
  42. package/dist/batch-map.d.ts.map +1 -0
  43. package/dist/batch-map.js +406 -0
  44. package/dist/batch-map.js.map +1 -0
  45. package/dist/batch-queue.d.ts +273 -0
  46. package/dist/batch-queue.d.ts.map +1 -0
  47. package/dist/batch-queue.js +271 -0
  48. package/dist/batch-queue.js.map +1 -0
  49. package/dist/context.d.ts +133 -0
  50. package/dist/context.d.ts.map +1 -0
  51. package/dist/context.js +267 -0
  52. package/dist/context.js.map +1 -0
  53. package/dist/embeddings.d.ts +123 -0
  54. package/dist/embeddings.d.ts.map +1 -0
  55. package/dist/embeddings.js +170 -0
  56. package/dist/embeddings.js.map +1 -0
  57. package/dist/eval/index.d.ts +8 -0
  58. package/dist/eval/index.d.ts.map +1 -0
  59. package/dist/eval/index.js +8 -0
  60. package/dist/eval/index.js.map +1 -0
  61. package/dist/eval/models.d.ts +66 -0
  62. package/dist/eval/models.d.ts.map +1 -0
  63. package/dist/eval/models.js +120 -0
  64. package/dist/eval/models.js.map +1 -0
  65. package/dist/eval/runner.d.ts +64 -0
  66. package/dist/eval/runner.d.ts.map +1 -0
  67. package/dist/eval/runner.js +148 -0
  68. package/dist/eval/runner.js.map +1 -0
  69. package/dist/generate.d.ts +168 -0
  70. package/dist/generate.d.ts.map +1 -0
  71. package/dist/generate.js +174 -0
  72. package/dist/generate.js.map +1 -0
  73. package/dist/index.d.ts +30 -0
  74. package/dist/index.d.ts.map +1 -0
  75. package/dist/index.js +54 -0
  76. package/dist/index.js.map +1 -0
  77. package/dist/primitives.d.ts +292 -0
  78. package/dist/primitives.d.ts.map +1 -0
  79. package/dist/primitives.js +471 -0
  80. package/dist/primitives.js.map +1 -0
  81. package/dist/providers/cloudflare.d.ts +9 -0
  82. package/dist/providers/cloudflare.d.ts.map +1 -0
  83. package/dist/providers/cloudflare.js +9 -0
  84. package/dist/providers/cloudflare.js.map +1 -0
  85. package/dist/providers/index.d.ts +9 -0
  86. package/dist/providers/index.d.ts.map +1 -0
  87. package/dist/providers/index.js +9 -0
  88. package/dist/providers/index.js.map +1 -0
  89. package/dist/schema.d.ts +54 -0
  90. package/dist/schema.d.ts.map +1 -0
  91. package/dist/schema.js +109 -0
  92. package/dist/schema.js.map +1 -0
  93. package/dist/template.d.ts +73 -0
  94. package/dist/template.d.ts.map +1 -0
  95. package/dist/template.js +129 -0
  96. package/dist/template.js.map +1 -0
  97. package/dist/types.d.ts +481 -0
  98. package/dist/types.d.ts.map +1 -0
  99. package/dist/types.js +5 -0
  100. package/dist/types.js.map +1 -0
  101. package/evalite.config.ts +19 -0
  102. package/evals/README.md +212 -0
  103. package/evals/classification.eval.ts +108 -0
  104. package/evals/marketing.eval.ts +370 -0
  105. package/evals/math.eval.ts +94 -0
  106. package/evals/run-evals.ts +166 -0
  107. package/evals/structured-output.eval.ts +143 -0
  108. package/evals/writing.eval.ts +117 -0
  109. package/examples/batch-blog-posts.ts +160 -0
  110. package/package.json +59 -43
  111. package/src/ai-promise.ts +784 -0
  112. package/src/ai.ts +1183 -0
  113. package/src/batch/anthropic.ts +375 -0
  114. package/src/batch/bedrock.ts +801 -0
  115. package/src/batch/cloudflare.ts +421 -0
  116. package/src/batch/google.ts +491 -0
  117. package/src/batch/index.ts +31 -0
  118. package/src/batch/memory.ts +253 -0
  119. package/src/batch/openai.ts +557 -0
  120. package/src/batch-map.ts +534 -0
  121. package/src/batch-queue.ts +493 -0
  122. package/src/context.ts +332 -0
  123. package/src/embeddings.ts +244 -0
  124. package/src/eval/index.ts +8 -0
  125. package/src/eval/models.ts +158 -0
  126. package/src/eval/runner.ts +217 -0
  127. package/src/generate.ts +245 -0
  128. package/src/index.ts +154 -0
  129. package/src/primitives.ts +612 -0
  130. package/src/providers/cloudflare.ts +15 -0
  131. package/src/providers/index.ts +14 -0
  132. package/src/schema.ts +147 -0
  133. package/src/template.ts +209 -0
  134. package/src/types.ts +540 -0
  135. package/test/README.md +105 -0
  136. package/test/ai-proxy.test.ts +192 -0
  137. package/test/async-iterators.test.ts +327 -0
  138. package/test/batch-background.test.ts +482 -0
  139. package/test/batch-blog-posts.test.ts +387 -0
  140. package/test/blog-generation.test.ts +510 -0
  141. package/test/browse-read.test.ts +611 -0
  142. package/test/core-functions.test.ts +694 -0
  143. package/test/decide.test.ts +393 -0
  144. package/test/define.test.ts +274 -0
  145. package/test/e2e-bedrock-manual.ts +163 -0
  146. package/test/e2e-bedrock.test.ts +191 -0
  147. package/test/e2e-flex-gateway.ts +157 -0
  148. package/test/e2e-flex-manual.ts +183 -0
  149. package/test/e2e-flex.test.ts +209 -0
  150. package/test/e2e-google-manual.ts +178 -0
  151. package/test/e2e-google.test.ts +216 -0
  152. package/test/embeddings.test.ts +284 -0
  153. package/test/evals/define-function.eval.test.ts +379 -0
  154. package/test/evals/primitives.eval.test.ts +384 -0
  155. package/test/function-types.test.ts +492 -0
  156. package/test/generate-core.test.ts +319 -0
  157. package/test/generate.test.ts +163 -0
  158. package/test/implicit-batch.test.ts +422 -0
  159. package/test/schema.test.ts +109 -0
  160. package/test/tagged-templates.test.ts +302 -0
  161. package/tsconfig.json +8 -6
  162. package/vitest.config.ts +42 -0
  163. package/LICENSE +0 -21
  164. package/db/cache.ts +0 -6
  165. package/db/mongo.ts +0 -75
  166. package/dist/mjs/db/cache.d.ts +0 -1
  167. package/dist/mjs/db/cache.js +0 -5
  168. package/dist/mjs/db/mongo.d.ts +0 -31
  169. package/dist/mjs/db/mongo.js +0 -48
  170. package/dist/mjs/examples/data.d.ts +0 -1105
  171. package/dist/mjs/examples/data.js +0 -1105
  172. package/dist/mjs/functions/ai.d.ts +0 -20
  173. package/dist/mjs/functions/ai.js +0 -83
  174. package/dist/mjs/functions/ai.test.d.ts +0 -1
  175. package/dist/mjs/functions/ai.test.js +0 -29
  176. package/dist/mjs/functions/gpt.d.ts +0 -4
  177. package/dist/mjs/functions/gpt.js +0 -10
  178. package/dist/mjs/functions/list.d.ts +0 -7
  179. package/dist/mjs/functions/list.js +0 -72
  180. package/dist/mjs/index.d.ts +0 -3
  181. package/dist/mjs/index.js +0 -3
  182. package/dist/mjs/queue/kafka.d.ts +0 -0
  183. package/dist/mjs/queue/kafka.js +0 -1
  184. package/dist/mjs/queue/memory.d.ts +0 -0
  185. package/dist/mjs/queue/memory.js +0 -1
  186. package/dist/mjs/queue/mongo.d.ts +0 -30
  187. package/dist/mjs/queue/mongo.js +0 -42
  188. package/dist/mjs/streams/kafka.d.ts +0 -0
  189. package/dist/mjs/streams/kafka.js +0 -1
  190. package/dist/mjs/streams/memory.d.ts +0 -0
  191. package/dist/mjs/streams/memory.js +0 -1
  192. package/dist/mjs/streams/mongo.d.ts +0 -0
  193. package/dist/mjs/streams/mongo.js +0 -1
  194. package/dist/mjs/streams/types.d.ts +0 -0
  195. package/dist/mjs/streams/types.js +0 -1
  196. package/dist/mjs/types.d.ts +0 -11
  197. package/dist/mjs/types.js +0 -1
  198. package/dist/mjs/utils/completion.d.ts +0 -9
  199. package/dist/mjs/utils/completion.js +0 -20
  200. package/dist/mjs/utils/schema.d.ts +0 -10
  201. package/dist/mjs/utils/schema.js +0 -72
  202. package/dist/mjs/utils/schema.test.d.ts +0 -1
  203. package/dist/mjs/utils/schema.test.js +0 -60
  204. package/dist/mjs/utils/state.d.ts +0 -1
  205. package/dist/mjs/utils/state.js +0 -19
  206. package/examples/data.ts +0 -1105
  207. package/fixup +0 -11
  208. package/functions/ai.test.ts +0 -41
  209. package/functions/ai.ts +0 -115
  210. package/functions/gpt.ts +0 -12
  211. package/functions/list.ts +0 -84
  212. package/index.ts +0 -3
  213. package/queue/kafka.ts +0 -0
  214. package/queue/memory.ts +0 -0
  215. package/queue/mongo.ts +0 -88
  216. package/streams/kafka.ts +0 -0
  217. package/streams/memory.ts +0 -0
  218. package/streams/mongo.ts +0 -0
  219. package/streams/types.ts +0 -0
  220. package/tsconfig-backup.json +0 -105
  221. package/tsconfig-base.json +0 -26
  222. package/tsconfig-cjs.json +0 -8
  223. package/types.ts +0 -12
  224. package/utils/completion.ts +0 -28
  225. package/utils/schema.test.ts +0 -69
  226. package/utils/schema.ts +0 -74
  227. package/utils/state.ts +0 -23
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Tests for the ai proxy and AI() schema functions
3
+ *
4
+ * These tests use real AI calls via the Cloudflare AI Gateway.
5
+ */
6
+
7
+ import { describe, it, expect, beforeEach } from 'vitest'
8
+ import { ai, AI, functions, withTemplate } from '../src/index.js'
9
+
10
+ // Skip tests if no gateway configured
11
+ const hasGateway = !!process.env.AI_GATEWAY_URL || !!process.env.ANTHROPIC_API_KEY
12
+
13
+ describe('ai proxy', () => {
14
+ beforeEach(() => {
15
+ functions.clear()
16
+ })
17
+
18
+ it('exposes functions registry', () => {
19
+ expect(ai.functions).toBeDefined()
20
+ expect(typeof ai.functions.list).toBe('function')
21
+ expect(typeof ai.functions.get).toBe('function')
22
+ expect(typeof ai.functions.set).toBe('function')
23
+ expect(typeof ai.functions.has).toBe('function')
24
+ expect(typeof ai.functions.clear).toBe('function')
25
+ expect(typeof ai.functions.delete).toBe('function')
26
+ })
27
+
28
+ it('exposes define helpers', () => {
29
+ expect(ai.define).toBeDefined()
30
+ expect(typeof ai.define).toBe('function')
31
+ expect(typeof ai.define.generative).toBe('function')
32
+ expect(typeof ai.define.agentic).toBe('function')
33
+ expect(typeof ai.define.human).toBe('function')
34
+ expect(typeof ai.define.code).toBe('function')
35
+ })
36
+
37
+ it('exposes defineFunction', () => {
38
+ expect(typeof ai.defineFunction).toBe('function')
39
+ })
40
+ })
41
+
42
+ describe.skipIf(!hasGateway)('ai proxy auto-define', () => {
43
+ beforeEach(() => {
44
+ functions.clear()
45
+ })
46
+
47
+ it('auto-defines a function on first call', async () => {
48
+ expect(functions.has('greetPerson')).toBe(false)
49
+
50
+ const result = await (ai as Record<string, (args: unknown) => Promise<unknown>>).greetPerson({
51
+ name: 'Alice',
52
+ style: 'friendly',
53
+ })
54
+
55
+ expect(result).toBeDefined()
56
+ expect(functions.has('greetPerson')).toBe(true)
57
+ })
58
+
59
+ it('uses cached definition on second call', async () => {
60
+ // First call - defines the function
61
+ await (ai as Record<string, (args: unknown) => Promise<unknown>>).capitalizeText({
62
+ text: 'hello',
63
+ })
64
+
65
+ const fn1 = functions.get('capitalizeText')
66
+ expect(fn1).toBeDefined()
67
+
68
+ // Second call - uses cached definition
69
+ await (ai as Record<string, (args: unknown) => Promise<unknown>>).capitalizeText({
70
+ text: 'world',
71
+ })
72
+
73
+ const fn2 = functions.get('capitalizeText')
74
+ expect(fn1).toBe(fn2) // Same cached function
75
+ })
76
+ })
77
+
78
+ describe.skipIf(!hasGateway)('AI() schema functions', () => {
79
+ it('creates schema-based functions', async () => {
80
+ const client = AI({
81
+ sentiment: {
82
+ sentiment: 'positive | negative | neutral',
83
+ score: 'Confidence score 0-1 (number)',
84
+ explanation: 'Brief explanation',
85
+ },
86
+ })
87
+
88
+ expect(client.sentiment).toBeDefined()
89
+ expect(typeof client.sentiment).toBe('function')
90
+ })
91
+
92
+ it('generates structured output from schema', async () => {
93
+ const client = AI({
94
+ person: {
95
+ name: 'Full name',
96
+ age: 'Age (number)',
97
+ occupation: 'Job title',
98
+ },
99
+ })
100
+
101
+ const result = await client.person('A software engineer named Alice who is 30')
102
+
103
+ expect(result).toBeDefined()
104
+ expect(typeof result.name).toBe('string')
105
+ expect(typeof result.age).toBe('number')
106
+ expect(typeof result.occupation).toBe('string')
107
+ })
108
+
109
+ it('generates nested objects', async () => {
110
+ const client = AI({
111
+ profile: {
112
+ user: {
113
+ name: 'Name',
114
+ email: 'Email address',
115
+ },
116
+ preferences: {
117
+ theme: 'light | dark',
118
+ notifications: 'Enabled? (boolean)',
119
+ },
120
+ },
121
+ })
122
+
123
+ const result = await client.profile('User Alice who prefers dark mode and has notifications on')
124
+
125
+ expect(result).toBeDefined()
126
+ expect(result.user).toBeDefined()
127
+ expect(result.preferences).toBeDefined()
128
+ expect(['light', 'dark']).toContain(result.preferences.theme)
129
+ expect(typeof result.preferences.notifications).toBe('boolean')
130
+ })
131
+
132
+ it('generates arrays', async () => {
133
+ const client = AI({
134
+ todoList: {
135
+ title: 'List title',
136
+ items: ['Todo items'],
137
+ priority: 'high | medium | low',
138
+ },
139
+ })
140
+
141
+ const result = await client.todoList('A high priority shopping list with 3 items')
142
+
143
+ expect(result).toBeDefined()
144
+ expect(typeof result.title).toBe('string')
145
+ expect(Array.isArray(result.items)).toBe(true)
146
+ expect(result.items.length).toBeGreaterThan(0)
147
+ expect(['high', 'medium', 'low']).toContain(result.priority)
148
+ })
149
+ })
150
+
151
+ describe('withTemplate helper', () => {
152
+ it('handles regular function calls', () => {
153
+ const fn = withTemplate((prompt: string) => prompt.toUpperCase())
154
+
155
+ const result = fn('hello world')
156
+ expect(result).toBe('HELLO WORLD')
157
+ })
158
+
159
+ it('handles tagged template literals', () => {
160
+ const fn = withTemplate((prompt: string) => prompt.toUpperCase())
161
+
162
+ const result = fn`hello world`
163
+ expect(result).toBe('HELLO WORLD')
164
+ })
165
+
166
+ it('handles tagged template literals with interpolation', () => {
167
+ const fn = withTemplate((prompt: string) => prompt.toUpperCase())
168
+
169
+ const name = 'Alice'
170
+ const result = fn`hello ${name}!`
171
+ expect(result).toBe('HELLO ALICE!')
172
+ })
173
+
174
+ it('handles multiple interpolations', () => {
175
+ const fn = withTemplate((prompt: string) => prompt)
176
+
177
+ const a = 'one'
178
+ const b = 'two'
179
+ const c = 'three'
180
+ const result = fn`${a}, ${b}, ${c}`
181
+ expect(result).toBe('one, two, three')
182
+ })
183
+
184
+ it('works with async functions', async () => {
185
+ const fn = withTemplate(async (prompt: string) => {
186
+ return `Result: ${prompt}`
187
+ })
188
+
189
+ const result = await fn`async test`
190
+ expect(result).toBe('Result: async test')
191
+ })
192
+ })
@@ -0,0 +1,327 @@
1
+ /**
2
+ * Tests for async iterator support on list and extract
3
+ *
4
+ * Functions that return lists can be streamed with `for await`:
5
+ * - list`...` - streams items as they're generated
6
+ * - extract`...` - streams extracted items
7
+ */
8
+
9
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
10
+
11
+ // ============================================================================
12
+ // Mock async generators
13
+ // ============================================================================
14
+
15
+ const mockStreamItems = vi.fn()
16
+
17
+ /**
18
+ * Create an async iterable from an array (simulates streaming)
19
+ */
20
+ async function* createAsyncIterable<T>(items: T[], delayMs = 10): AsyncIterable<T> {
21
+ for (const item of items) {
22
+ await new Promise(resolve => setTimeout(resolve, delayMs))
23
+ yield item
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Mock list function that returns both a promise and an async iterable
29
+ */
30
+ function createMockStreamingList() {
31
+ return function list(promptOrStrings: string | TemplateStringsArray, ...args: unknown[]) {
32
+ let prompt: string
33
+
34
+ if (Array.isArray(promptOrStrings) && 'raw' in promptOrStrings) {
35
+ prompt = (promptOrStrings as TemplateStringsArray).reduce((acc, str, i) => {
36
+ return acc + str + (args[i] ?? '')
37
+ }, '')
38
+ } else {
39
+ prompt = promptOrStrings as string
40
+ }
41
+
42
+ const items = mockStreamItems(prompt)
43
+
44
+ // Return an object that is both a Promise and AsyncIterable
45
+ const asyncIterable = createAsyncIterable(items)
46
+
47
+ const result = {
48
+ // Promise interface - resolve to full array
49
+ then: (resolve: (value: string[]) => void, reject?: (error: Error) => void) => {
50
+ return Promise.resolve(items).then(resolve, reject)
51
+ },
52
+
53
+ // AsyncIterable interface - stream items
54
+ [Symbol.asyncIterator]: () => asyncIterable[Symbol.asyncIterator](),
55
+ }
56
+
57
+ return result as Promise<string[]> & AsyncIterable<string>
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Mock extract function with streaming support
63
+ */
64
+ function createMockStreamingExtract() {
65
+ return function extract(promptOrStrings: string | TemplateStringsArray, ...args: unknown[]) {
66
+ let prompt: string
67
+
68
+ if (Array.isArray(promptOrStrings) && 'raw' in promptOrStrings) {
69
+ prompt = (promptOrStrings as TemplateStringsArray).reduce((acc, str, i) => {
70
+ return acc + str + (args[i] ?? '')
71
+ }, '')
72
+ } else {
73
+ prompt = promptOrStrings as string
74
+ }
75
+
76
+ const items = mockStreamItems(prompt)
77
+ const asyncIterable = createAsyncIterable(items)
78
+
79
+ const result = {
80
+ then: (resolve: (value: string[]) => void, reject?: (error: Error) => void) => {
81
+ return Promise.resolve(items).then(resolve, reject)
82
+ },
83
+ [Symbol.asyncIterator]: () => asyncIterable[Symbol.asyncIterator](),
84
+ }
85
+
86
+ return result as Promise<string[]> & AsyncIterable<string>
87
+ }
88
+ }
89
+
90
+ // ============================================================================
91
+ // list() async iterator tests
92
+ // ============================================================================
93
+
94
+ describe('list() async iteration', () => {
95
+ beforeEach(() => {
96
+ mockStreamItems.mockReset()
97
+ })
98
+
99
+ it('can be awaited to get full array', async () => {
100
+ mockStreamItems.mockReturnValue(['Idea 1', 'Idea 2', 'Idea 3'])
101
+ const list = createMockStreamingList()
102
+
103
+ const result = await list`startup ideas`
104
+
105
+ expect(Array.isArray(result)).toBe(true)
106
+ expect(result).toHaveLength(3)
107
+ })
108
+
109
+ it('can be iterated with for await', async () => {
110
+ mockStreamItems.mockReturnValue(['Idea 1', 'Idea 2', 'Idea 3'])
111
+ const list = createMockStreamingList()
112
+
113
+ const collected: string[] = []
114
+ for await (const item of list`startup ideas`) {
115
+ collected.push(item)
116
+ }
117
+
118
+ expect(collected).toHaveLength(3)
119
+ expect(collected).toEqual(['Idea 1', 'Idea 2', 'Idea 3'])
120
+ })
121
+
122
+ it('allows early termination with break', async () => {
123
+ mockStreamItems.mockReturnValue(['Idea 1', 'Billion Dollar Idea', 'Idea 3', 'Idea 4', 'Idea 5'])
124
+ const list = createMockStreamingList()
125
+
126
+ const collected: string[] = []
127
+ for await (const idea of list`startup ideas`) {
128
+ collected.push(idea)
129
+ if (idea.includes('Billion')) {
130
+ break
131
+ }
132
+ }
133
+
134
+ // Should have stopped after finding the billion dollar idea
135
+ expect(collected).toHaveLength(2)
136
+ expect(collected[1]).toBe('Billion Dollar Idea')
137
+ })
138
+
139
+ it('supports nested iteration pattern from README', async () => {
140
+ const marketList = createMockStreamingList()
141
+ const icpList = createMockStreamingList()
142
+
143
+ // Simulate the nested pattern
144
+ mockStreamItems
145
+ .mockReturnValueOnce(['Market A', 'Market B'])
146
+ .mockReturnValueOnce(['ICP 1', 'ICP 2'])
147
+ .mockReturnValueOnce(['ICP 3', 'ICP 4'])
148
+
149
+ const results: Array<{ market: string; icp: string }> = []
150
+
151
+ for await (const market of marketList`market segments`) {
152
+ for await (const icp of icpList`customer profiles for ${market}`) {
153
+ results.push({ market, icp })
154
+ }
155
+ }
156
+
157
+ expect(results).toHaveLength(4)
158
+ expect(results[0]).toEqual({ market: 'Market A', icp: 'ICP 1' })
159
+ expect(results[3]).toEqual({ market: 'Market B', icp: 'ICP 4' })
160
+ })
161
+
162
+ it('processes items as they stream in', async () => {
163
+ mockStreamItems.mockReturnValue(['Item 1', 'Item 2', 'Item 3'])
164
+ const list = createMockStreamingList()
165
+
166
+ const processedAt: number[] = []
167
+ const startTime = Date.now()
168
+
169
+ for await (const _item of list`items`) {
170
+ processedAt.push(Date.now() - startTime)
171
+ }
172
+
173
+ // Items should be processed incrementally, not all at once
174
+ expect(processedAt[1]).toBeGreaterThan(processedAt[0])
175
+ expect(processedAt[2]).toBeGreaterThan(processedAt[1])
176
+ })
177
+ })
178
+
179
+ // ============================================================================
180
+ // extract() async iterator tests
181
+ // ============================================================================
182
+
183
+ describe('extract() async iteration', () => {
184
+ beforeEach(() => {
185
+ mockStreamItems.mockReset()
186
+ })
187
+
188
+ it('can be awaited to get full array', async () => {
189
+ mockStreamItems.mockReturnValue(['John Smith', 'Jane Doe', 'Bob Wilson'])
190
+ const extract = createMockStreamingExtract()
191
+
192
+ const result = await extract`names from article`
193
+
194
+ expect(Array.isArray(result)).toBe(true)
195
+ expect(result).toHaveLength(3)
196
+ })
197
+
198
+ it('can be iterated with for await', async () => {
199
+ mockStreamItems.mockReturnValue(['email1@example.com', 'email2@example.com'])
200
+ const extract = createMockStreamingExtract()
201
+
202
+ const collected: string[] = []
203
+ for await (const email of extract`email addresses from document`) {
204
+ collected.push(email)
205
+ }
206
+
207
+ expect(collected).toHaveLength(2)
208
+ })
209
+
210
+ it('supports the research + extract pattern from README', async () => {
211
+ mockStreamItems.mockReturnValue(['Competitor A', 'Competitor B', 'Competitor C'])
212
+ const extract = createMockStreamingExtract()
213
+
214
+ const competitors: string[] = []
215
+ const marketResearch = 'Report mentioning Competitor A, Competitor B, and Competitor C...'
216
+
217
+ for await (const competitor of extract`company names from ${marketResearch}`) {
218
+ competitors.push(competitor)
219
+ // In real code, you would do: await research`${competitor} vs ${ourProduct}`
220
+ }
221
+
222
+ expect(competitors).toHaveLength(3)
223
+ })
224
+
225
+ it('allows processing each extraction as it completes', async () => {
226
+ mockStreamItems.mockReturnValue(['email1@test.com', 'email2@test.com'])
227
+ const extract = createMockStreamingExtract()
228
+
229
+ const notifications: string[] = []
230
+ for await (const email of extract`emails from document`) {
231
+ // Simulate sending notification
232
+ notifications.push(`Notified: ${email}`)
233
+ }
234
+
235
+ expect(notifications).toHaveLength(2)
236
+ expect(notifications[0]).toBe('Notified: email1@test.com')
237
+ })
238
+ })
239
+
240
+ // ============================================================================
241
+ // Combined patterns
242
+ // ============================================================================
243
+
244
+ describe('combined async iteration patterns', () => {
245
+ beforeEach(() => {
246
+ mockStreamItems.mockReset()
247
+ })
248
+
249
+ it('supports the full market research pattern from README', async () => {
250
+ const list = createMockStreamingList()
251
+ const extract = createMockStreamingExtract()
252
+
253
+ // Mock different results for each call
254
+ mockStreamItems
255
+ .mockReturnValueOnce(['Market A']) // markets
256
+ .mockReturnValueOnce(['ICP 1']) // ICPs for Market A
257
+ .mockReturnValueOnce(['Blog 1', 'Blog 2']) // blog titles
258
+
259
+ const results: string[] = []
260
+
261
+ // Simplified version of README example
262
+ for await (const market of list`market segments for idea`) {
263
+ for await (const icp of list`customer profiles for ${market}`) {
264
+ for await (const title of list`blog titles for ${icp}`) {
265
+ results.push(`${market} > ${icp} > ${title}`)
266
+ }
267
+ }
268
+ }
269
+
270
+ expect(results).toHaveLength(2)
271
+ expect(results[0]).toBe('Market A > ICP 1 > Blog 1')
272
+ expect(results[1]).toBe('Market A > ICP 1 > Blog 2')
273
+ })
274
+ })
275
+
276
+ // ============================================================================
277
+ // Type safety
278
+ // ============================================================================
279
+
280
+ describe('async iterator type safety', () => {
281
+ it('list returns string items by default', async () => {
282
+ mockStreamItems.mockReturnValue(['a', 'b', 'c'])
283
+ const list = createMockStreamingList()
284
+
285
+ for await (const item of list`items`) {
286
+ expect(typeof item).toBe('string')
287
+ }
288
+ })
289
+
290
+ it('extract can return typed objects with schema', async () => {
291
+ // This tests the concept - actual implementation would use schema
292
+ const items = [
293
+ { name: 'Acme', role: 'competitor' },
294
+ { name: 'Beta', role: 'partner' },
295
+ ]
296
+
297
+ mockStreamItems.mockReturnValue(items)
298
+ const extract = createMockStreamingExtract()
299
+
300
+ const collected: Array<{ name: string; role: string }> = []
301
+ for await (const company of extract`companies from text`) {
302
+ collected.push(company as { name: string; role: string })
303
+ }
304
+
305
+ expect(collected[0]).toHaveProperty('name')
306
+ expect(collected[0]).toHaveProperty('role')
307
+ })
308
+ })
309
+
310
+ // ============================================================================
311
+ // Error handling
312
+ // ============================================================================
313
+
314
+ describe('async iterator error handling', () => {
315
+ it('propagates errors during iteration', async () => {
316
+ mockStreamItems.mockImplementation(() => {
317
+ throw new Error('Generation failed')
318
+ })
319
+ const list = createMockStreamingList()
320
+
321
+ await expect(async () => {
322
+ for await (const _item of list`items`) {
323
+ // Should not reach here
324
+ }
325
+ }).rejects.toThrow('Generation failed')
326
+ })
327
+ })