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.
Files changed (286) hide show
  1. package/.turbo/turbo-build.log +1 -4
  2. package/CHANGELOG.md +68 -1
  3. package/README.md +397 -157
  4. package/dist/ai-promise.d.ts +50 -3
  5. package/dist/ai-promise.d.ts.map +1 -1
  6. package/dist/ai-promise.js +410 -51
  7. package/dist/ai-promise.js.map +1 -1
  8. package/dist/ai-schemas.d.ts +56 -0
  9. package/dist/ai-schemas.d.ts.map +1 -0
  10. package/dist/ai-schemas.js +53 -0
  11. package/dist/ai-schemas.js.map +1 -0
  12. package/dist/ai.d.ts +16 -242
  13. package/dist/ai.d.ts.map +1 -1
  14. package/dist/ai.js +54 -837
  15. package/dist/ai.js.map +1 -1
  16. package/dist/batch/anthropic.d.ts +6 -4
  17. package/dist/batch/anthropic.d.ts.map +1 -1
  18. package/dist/batch/anthropic.js +83 -145
  19. package/dist/batch/anthropic.js.map +1 -1
  20. package/dist/batch/bedrock.d.ts +8 -30
  21. package/dist/batch/bedrock.d.ts.map +1 -1
  22. package/dist/batch/bedrock.js +155 -338
  23. package/dist/batch/bedrock.js.map +1 -1
  24. package/dist/batch/cloudflare.d.ts +8 -20
  25. package/dist/batch/cloudflare.d.ts.map +1 -1
  26. package/dist/batch/cloudflare.js +68 -189
  27. package/dist/batch/cloudflare.js.map +1 -1
  28. package/dist/batch/google.d.ts +6 -20
  29. package/dist/batch/google.d.ts.map +1 -1
  30. package/dist/batch/google.js +70 -238
  31. package/dist/batch/google.js.map +1 -1
  32. package/dist/batch/index.d.ts +4 -1
  33. package/dist/batch/index.d.ts.map +1 -1
  34. package/dist/batch/index.js +4 -1
  35. package/dist/batch/index.js.map +1 -1
  36. package/dist/batch/memory.d.ts +1 -1
  37. package/dist/batch/memory.d.ts.map +1 -1
  38. package/dist/batch/memory.js +14 -10
  39. package/dist/batch/memory.js.map +1 -1
  40. package/dist/batch/openai.d.ts +11 -14
  41. package/dist/batch/openai.d.ts.map +1 -1
  42. package/dist/batch/openai.js +52 -156
  43. package/dist/batch/openai.js.map +1 -1
  44. package/dist/batch/provider.d.ts +111 -0
  45. package/dist/batch/provider.d.ts.map +1 -0
  46. package/dist/batch/provider.js +233 -0
  47. package/dist/batch/provider.js.map +1 -0
  48. package/dist/batch-map.d.ts.map +1 -1
  49. package/dist/batch-map.js +23 -17
  50. package/dist/batch-map.js.map +1 -1
  51. package/dist/batch-queue.d.ts +65 -0
  52. package/dist/batch-queue.d.ts.map +1 -1
  53. package/dist/batch-queue.js +169 -14
  54. package/dist/batch-queue.js.map +1 -1
  55. package/dist/budget.d.ts +272 -0
  56. package/dist/budget.d.ts.map +1 -0
  57. package/dist/budget.js +513 -0
  58. package/dist/budget.js.map +1 -0
  59. package/dist/cache.d.ts +295 -0
  60. package/dist/cache.d.ts.map +1 -0
  61. package/dist/cache.js +433 -0
  62. package/dist/cache.js.map +1 -0
  63. package/dist/context.d.ts +42 -8
  64. package/dist/context.d.ts.map +1 -1
  65. package/dist/context.js +64 -62
  66. package/dist/context.js.map +1 -1
  67. package/dist/digital-objects-registry.d.ts +229 -0
  68. package/dist/digital-objects-registry.d.ts.map +1 -0
  69. package/dist/digital-objects-registry.js +617 -0
  70. package/dist/digital-objects-registry.js.map +1 -0
  71. package/dist/embeddings.d.ts +2 -2
  72. package/dist/embeddings.d.ts.map +1 -1
  73. package/dist/errors.d.ts +22 -0
  74. package/dist/errors.d.ts.map +1 -0
  75. package/dist/errors.js +35 -0
  76. package/dist/errors.js.map +1 -0
  77. package/dist/eval/runner.d.ts +10 -1
  78. package/dist/eval/runner.d.ts.map +1 -1
  79. package/dist/eval/runner.js +41 -35
  80. package/dist/eval/runner.js.map +1 -1
  81. package/dist/eval-log/in-memory.d.ts +34 -0
  82. package/dist/eval-log/in-memory.d.ts.map +1 -0
  83. package/dist/eval-log/in-memory.js +84 -0
  84. package/dist/eval-log/in-memory.js.map +1 -0
  85. package/dist/eval-log/index.d.ts +29 -0
  86. package/dist/eval-log/index.d.ts.map +1 -0
  87. package/dist/eval-log/index.js +39 -0
  88. package/dist/eval-log/index.js.map +1 -0
  89. package/dist/eval-log/types.d.ts +101 -0
  90. package/dist/eval-log/types.d.ts.map +1 -0
  91. package/dist/eval-log/types.js +16 -0
  92. package/dist/eval-log/types.js.map +1 -0
  93. package/dist/function-registry.d.ts +116 -0
  94. package/dist/function-registry.d.ts.map +1 -0
  95. package/dist/function-registry.js +546 -0
  96. package/dist/function-registry.js.map +1 -0
  97. package/dist/generate.d.ts +9 -3
  98. package/dist/generate.d.ts.map +1 -1
  99. package/dist/generate.js +18 -22
  100. package/dist/generate.js.map +1 -1
  101. package/dist/index.d.ts +35 -20
  102. package/dist/index.d.ts.map +1 -1
  103. package/dist/index.js +89 -42
  104. package/dist/index.js.map +1 -1
  105. package/dist/logger.d.ts +118 -0
  106. package/dist/logger.d.ts.map +1 -0
  107. package/dist/logger.js +187 -0
  108. package/dist/logger.js.map +1 -0
  109. package/dist/middleware/budget.d.ts +84 -0
  110. package/dist/middleware/budget.d.ts.map +1 -0
  111. package/dist/middleware/budget.js +110 -0
  112. package/dist/middleware/budget.js.map +1 -0
  113. package/dist/middleware/cache.d.ts +103 -0
  114. package/dist/middleware/cache.d.ts.map +1 -0
  115. package/dist/middleware/cache.js +228 -0
  116. package/dist/middleware/cache.js.map +1 -0
  117. package/dist/middleware/embed-cache.d.ts +99 -0
  118. package/dist/middleware/embed-cache.d.ts.map +1 -0
  119. package/dist/middleware/embed-cache.js +128 -0
  120. package/dist/middleware/embed-cache.js.map +1 -0
  121. package/dist/middleware/index.d.ts +11 -0
  122. package/dist/middleware/index.d.ts.map +1 -0
  123. package/dist/middleware/index.js +11 -0
  124. package/dist/middleware/index.js.map +1 -0
  125. package/dist/middleware/trace.d.ts +103 -0
  126. package/dist/middleware/trace.d.ts.map +1 -0
  127. package/dist/middleware/trace.js +176 -0
  128. package/dist/middleware/trace.js.map +1 -0
  129. package/dist/primitives.d.ts +120 -1
  130. package/dist/primitives.d.ts.map +1 -1
  131. package/dist/primitives.js +398 -26
  132. package/dist/primitives.js.map +1 -1
  133. package/dist/retry.d.ts +368 -0
  134. package/dist/retry.d.ts.map +1 -0
  135. package/dist/retry.js +646 -0
  136. package/dist/retry.js.map +1 -0
  137. package/dist/schema.d.ts.map +1 -1
  138. package/dist/schema.js +2 -10
  139. package/dist/schema.js.map +1 -1
  140. package/dist/telemetry.d.ts +128 -0
  141. package/dist/telemetry.d.ts.map +1 -0
  142. package/dist/telemetry.js +285 -0
  143. package/dist/telemetry.js.map +1 -0
  144. package/dist/template.d.ts.map +1 -1
  145. package/dist/template.js +6 -1
  146. package/dist/template.js.map +1 -1
  147. package/dist/tool-orchestration.d.ts +453 -0
  148. package/dist/tool-orchestration.d.ts.map +1 -0
  149. package/dist/tool-orchestration.js +763 -0
  150. package/dist/tool-orchestration.js.map +1 -0
  151. package/dist/type-guards.d.ts +28 -0
  152. package/dist/type-guards.d.ts.map +1 -0
  153. package/dist/type-guards.js +29 -0
  154. package/dist/type-guards.js.map +1 -0
  155. package/dist/types.d.ts +135 -17
  156. package/dist/types.d.ts.map +1 -1
  157. package/dist/types.js +36 -1
  158. package/dist/types.js.map +1 -1
  159. package/dist/wrap-for-v3.d.ts +80 -0
  160. package/dist/wrap-for-v3.d.ts.map +1 -0
  161. package/dist/wrap-for-v3.js +89 -0
  162. package/dist/wrap-for-v3.js.map +1 -0
  163. package/examples/00-quickstart.ts +232 -0
  164. package/examples/01-rag-chatbot.ts +212 -0
  165. package/examples/02-multi-agent-research.ts +290 -0
  166. package/examples/03-email-classification.ts +379 -0
  167. package/examples/04-content-moderation.ts +400 -0
  168. package/examples/05-document-extraction.ts +455 -0
  169. package/examples/06-streaming-chat-nextjs.ts +437 -0
  170. package/examples/07-cloudflare-worker.ts +483 -0
  171. package/examples/08-batch-processing.ts +491 -0
  172. package/examples/09-budget-constrained.ts +527 -0
  173. package/examples/10-tool-orchestration.ts +565 -0
  174. package/examples/11-retry-resilience.ts +403 -0
  175. package/examples/12-caching-strategies.ts +422 -0
  176. package/examples/README.md +145 -0
  177. package/package.json +10 -6
  178. package/src/ai-promise.ts +528 -99
  179. package/src/ai-schemas.ts +122 -0
  180. package/src/ai.ts +69 -1153
  181. package/src/batch/anthropic.ts +96 -161
  182. package/src/batch/bedrock.ts +203 -454
  183. package/src/batch/cloudflare.ts +99 -282
  184. package/src/batch/google.ts +91 -297
  185. package/src/batch/index.ts +4 -1
  186. package/src/batch/memory.ts +15 -10
  187. package/src/batch/openai.ts +65 -193
  188. package/src/batch/provider.ts +336 -0
  189. package/src/batch-map.ts +29 -24
  190. package/src/batch-queue.ts +200 -11
  191. package/src/budget.ts +740 -0
  192. package/src/cache.ts +681 -0
  193. package/src/context.ts +122 -76
  194. package/src/digital-objects-registry.ts +750 -0
  195. package/src/errors.ts +37 -0
  196. package/src/eval/runner.ts +63 -38
  197. package/src/eval-log/in-memory.ts +90 -0
  198. package/src/eval-log/index.ts +46 -0
  199. package/src/eval-log/types.ts +110 -0
  200. package/src/function-registry.ts +671 -0
  201. package/src/generate.ts +33 -33
  202. package/src/index.ts +325 -49
  203. package/src/logger.ts +232 -0
  204. package/src/middleware/budget.ts +171 -0
  205. package/src/middleware/cache.ts +299 -0
  206. package/src/middleware/embed-cache.ts +195 -0
  207. package/src/middleware/index.ts +23 -0
  208. package/src/middleware/trace.ts +248 -0
  209. package/src/primitives.ts +589 -62
  210. package/src/retry.ts +902 -0
  211. package/src/schema.ts +8 -17
  212. package/src/telemetry.ts +403 -0
  213. package/src/template.ts +8 -4
  214. package/src/tool-orchestration.ts +1173 -0
  215. package/src/type-guards.ts +31 -0
  216. package/src/types.ts +164 -25
  217. package/src/wrap-for-v3.ts +105 -0
  218. package/test/ai-promise.test.ts +1080 -0
  219. package/test/ai-proxy.test.ts +1 -1
  220. package/test/backward-compat.test.ts +147 -0
  221. package/test/batch-autosubmit-errors.test.ts +610 -0
  222. package/test/batch-blog-posts.test.ts +87 -129
  223. package/test/budget-tracking.test.ts +800 -0
  224. package/test/cache.test.ts +712 -0
  225. package/test/context-isolation.test.ts +687 -0
  226. package/test/core-functions.test.ts +183 -579
  227. package/test/decide.test.ts +154 -322
  228. package/test/define.test.ts +211 -8
  229. package/test/digital-objects-registry.test.ts +760 -0
  230. package/test/embedding-cache-middleware.test.ts +140 -0
  231. package/test/evals/deterministic.eval.test.ts +376 -0
  232. package/test/generate-core.test.ts +140 -229
  233. package/test/implicit-batch.test.ts +22 -65
  234. package/test/json-parse-error-handling.test.ts +463 -0
  235. package/test/retry-policy-integration.test.ts +117 -0
  236. package/test/retry.test.ts +1016 -0
  237. package/test/schema.test.ts +55 -19
  238. package/test/streaming.test.ts +316 -0
  239. package/test/template.test.ts +1164 -0
  240. package/test/tool-orchestration.test.ts +1040 -0
  241. package/test/wrap-for-v3.test.ts +612 -0
  242. package/vitest.config.js +6 -0
  243. package/vitest.config.ts +20 -0
  244. package/dist/rpc/auth.d.ts +0 -69
  245. package/dist/rpc/auth.d.ts.map +0 -1
  246. package/dist/rpc/auth.js +0 -136
  247. package/dist/rpc/auth.js.map +0 -1
  248. package/dist/rpc/client.d.ts +0 -62
  249. package/dist/rpc/client.d.ts.map +0 -1
  250. package/dist/rpc/client.js +0 -103
  251. package/dist/rpc/client.js.map +0 -1
  252. package/dist/rpc/deferred.d.ts +0 -60
  253. package/dist/rpc/deferred.d.ts.map +0 -1
  254. package/dist/rpc/deferred.js +0 -96
  255. package/dist/rpc/deferred.js.map +0 -1
  256. package/dist/rpc/index.d.ts +0 -22
  257. package/dist/rpc/index.d.ts.map +0 -1
  258. package/dist/rpc/index.js +0 -38
  259. package/dist/rpc/index.js.map +0 -1
  260. package/dist/rpc/local.d.ts +0 -42
  261. package/dist/rpc/local.d.ts.map +0 -1
  262. package/dist/rpc/local.js +0 -50
  263. package/dist/rpc/local.js.map +0 -1
  264. package/dist/rpc/server.d.ts +0 -165
  265. package/dist/rpc/server.d.ts.map +0 -1
  266. package/dist/rpc/server.js +0 -405
  267. package/dist/rpc/server.js.map +0 -1
  268. package/dist/rpc/session.d.ts +0 -32
  269. package/dist/rpc/session.d.ts.map +0 -1
  270. package/dist/rpc/session.js +0 -43
  271. package/dist/rpc/session.js.map +0 -1
  272. package/dist/rpc/transport.d.ts +0 -306
  273. package/dist/rpc/transport.d.ts.map +0 -1
  274. package/dist/rpc/transport.js +0 -731
  275. package/dist/rpc/transport.js.map +0 -1
  276. package/src/batch/anthropic.js +0 -256
  277. package/src/batch/bedrock.js +0 -584
  278. package/src/batch/cloudflare.js +0 -287
  279. package/src/batch/google.js +0 -359
  280. package/src/batch/index.js +0 -30
  281. package/src/batch/memory.js +0 -187
  282. package/src/batch/openai.js +0 -402
  283. package/src/eval/index.js +0 -7
  284. package/src/eval/models.js +0 -119
  285. package/src/eval/runner.js +0 -147
  286. package/test/schema.test.js +0 -96
@@ -0,0 +1,1080 @@
1
+ /**
2
+ * Tests for AIPromise module
3
+ *
4
+ * Comprehensive tests covering:
5
+ * - AIPromise class construction and properties
6
+ * - Promise pipelining behavior
7
+ * - Property access tracking for schema inference
8
+ * - Dependency resolution
9
+ * - Batch map recording and replay
10
+ * - Streaming interfaces (StreamingAIPromise)
11
+ * - Error propagation through promise chains
12
+ * - Factory functions
13
+ * - Template tag helpers
14
+ *
15
+ * @packageDocumentation
16
+ */
17
+
18
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
19
+ import {
20
+ AIPromise,
21
+ AI_PROMISE_SYMBOL,
22
+ RAW_PROMISE_SYMBOL,
23
+ isAIPromise,
24
+ getRawPromise,
25
+ createTextPromise,
26
+ createObjectPromise,
27
+ createListPromise,
28
+ createListsPromise,
29
+ createBooleanPromise,
30
+ createExtractPromise,
31
+ parseTemplateWithDependencies,
32
+ createAITemplateFunction,
33
+ type AIPromiseOptions,
34
+ type StreamingAIPromise,
35
+ } from '../src/ai-promise.js'
36
+ import {
37
+ createBatchMap,
38
+ BatchMapPromise,
39
+ isInRecordingMode,
40
+ getCurrentItemPlaceholder,
41
+ captureOperation,
42
+ isBatchMapPromise,
43
+ BATCH_MAP_SYMBOL,
44
+ } from '../src/batch-map.js'
45
+
46
+ // Skip integration tests that require AI gateway
47
+ const hasGateway = !!process.env.AI_GATEWAY_URL || !!process.env.ANTHROPIC_API_KEY
48
+
49
+ // ============================================================================
50
+ // 1. AIPromise Class Construction and Identification
51
+ // ============================================================================
52
+
53
+ describe('AIPromise Construction and Identification', () => {
54
+ describe('constructor', () => {
55
+ it('creates an AIPromise with a prompt', () => {
56
+ const promise = new AIPromise<string>('test prompt')
57
+ expect(promise.prompt).toBe('test prompt')
58
+ })
59
+
60
+ it('creates an AIPromise with options', () => {
61
+ const promise = new AIPromise<string>('test prompt', {
62
+ type: 'text',
63
+ model: 'sonnet',
64
+ temperature: 0.7,
65
+ })
66
+ expect(promise.prompt).toBe('test prompt')
67
+ })
68
+
69
+ it('initializes with empty property path', () => {
70
+ const promise = new AIPromise<string>('test prompt')
71
+ expect(promise.path).toEqual([])
72
+ })
73
+
74
+ it('initializes with provided property path', () => {
75
+ const promise = new AIPromise<string>('test prompt', {
76
+ propertyPath: ['foo', 'bar'],
77
+ })
78
+ expect(promise.path).toEqual(['foo', 'bar'])
79
+ })
80
+
81
+ it('marks the promise with AI_PROMISE_SYMBOL', () => {
82
+ const promise = new AIPromise<string>('test prompt')
83
+ expect((promise as any)[AI_PROMISE_SYMBOL]).toBe(true)
84
+ })
85
+
86
+ it('starts with isResolved = false', () => {
87
+ const promise = new AIPromise<string>('test prompt')
88
+ expect(promise.isResolved).toBe(false)
89
+ })
90
+
91
+ it('starts with empty accessedProps', () => {
92
+ const promise = new AIPromise<string>('test prompt')
93
+ expect(promise.accessedProps.size).toBe(0)
94
+ })
95
+ })
96
+
97
+ describe('isAIPromise helper', () => {
98
+ it('returns true for AIPromise instances', () => {
99
+ const promise = new AIPromise<string>('test')
100
+ expect(isAIPromise(promise)).toBe(true)
101
+ })
102
+
103
+ it('returns false for regular promises', () => {
104
+ const promise = Promise.resolve('test')
105
+ expect(isAIPromise(promise)).toBe(false)
106
+ })
107
+
108
+ it('returns false for null', () => {
109
+ expect(isAIPromise(null)).toBe(false)
110
+ })
111
+
112
+ it('returns false for undefined', () => {
113
+ expect(isAIPromise(undefined)).toBe(false)
114
+ })
115
+
116
+ it('returns false for plain objects', () => {
117
+ expect(isAIPromise({ test: 'value' })).toBe(false)
118
+ })
119
+
120
+ it('returns false for primitives', () => {
121
+ expect(isAIPromise('string')).toBe(false)
122
+ expect(isAIPromise(123)).toBe(false)
123
+ expect(isAIPromise(true)).toBe(false)
124
+ })
125
+ })
126
+
127
+ describe('getRawPromise helper', () => {
128
+ it('returns the raw AIPromise from a proxied value', () => {
129
+ const promise = new AIPromise<{ name: string }>('test')
130
+ // Access a property to get a proxied child promise
131
+ const childPromise = (promise as any).name as AIPromise<string>
132
+
133
+ // getRawPromise should return the underlying promise
134
+ const raw = getRawPromise(childPromise)
135
+ expect(isAIPromise(raw)).toBe(true)
136
+ })
137
+
138
+ it('returns the same promise if already raw', () => {
139
+ const promise = new AIPromise<string>('test')
140
+ const raw = getRawPromise(promise)
141
+ expect(raw.prompt).toBe('test')
142
+ })
143
+ })
144
+ })
145
+
146
+ // ============================================================================
147
+ // 2. Property Access Tracking for Schema Inference
148
+ // ============================================================================
149
+
150
+ describe('Property Access Tracking', () => {
151
+ describe('basic property access', () => {
152
+ it('tracks accessed properties on the promise', () => {
153
+ const promise = new AIPromise<{ name: string; age: number }>('test')
154
+
155
+ // Access properties through destructuring
156
+ const { name, age } = promise as any
157
+
158
+ expect(promise.accessedProps.has('name')).toBe(true)
159
+ expect(promise.accessedProps.has('age')).toBe(true)
160
+ })
161
+
162
+ it('tracks nested property access', () => {
163
+ const promise = new AIPromise<{ user: { name: string } }>('test')
164
+
165
+ // Access nested property
166
+ const user = (promise as any).user
167
+ expect(promise.accessedProps.has('user')).toBe(true)
168
+ })
169
+
170
+ it('returns a new AIPromise for property access', () => {
171
+ const promise = new AIPromise<{ name: string }>('test')
172
+ const nameProp = (promise as any).name
173
+
174
+ expect(isAIPromise(nameProp)).toBe(true)
175
+ expect(nameProp.path).toEqual(['name'])
176
+ })
177
+
178
+ it('builds property path for nested access', () => {
179
+ const promise = new AIPromise<{ user: { profile: { name: string } } }>('test')
180
+ const name = (promise as any).user.profile.name
181
+
182
+ expect(name.path).toEqual(['user', 'profile', 'name'])
183
+ })
184
+ })
185
+
186
+ describe('proxy behavior', () => {
187
+ it('prevents property mutation', () => {
188
+ const promise = new AIPromise<{ name: string }>('test')
189
+
190
+ expect(() => {
191
+ ;(promise as any).name = 'new value'
192
+ }).toThrow('AIPromise properties are read-only')
193
+ })
194
+
195
+ it('prevents property deletion', () => {
196
+ const promise = new AIPromise<{ name: string }>('test')
197
+
198
+ expect(() => {
199
+ delete (promise as any).name
200
+ }).toThrow('AIPromise properties cannot be deleted')
201
+ })
202
+
203
+ it('allows access to internal properties', () => {
204
+ const promise = new AIPromise<string>('test prompt')
205
+
206
+ expect(promise.prompt).toBe('test prompt')
207
+ expect(promise.path).toEqual([])
208
+ expect(promise.isResolved).toBe(false)
209
+ expect(promise.accessedProps).toBeDefined()
210
+ })
211
+
212
+ it('allows access to promise methods', () => {
213
+ const promise = new AIPromise<string>('test')
214
+
215
+ expect(typeof promise.then).toBe('function')
216
+ expect(typeof promise.catch).toBe('function')
217
+ expect(typeof promise.finally).toBe('function')
218
+ })
219
+
220
+ it('allows access to AIPromise methods', () => {
221
+ const promise = new AIPromise<string[]>('test', { type: 'list' })
222
+
223
+ expect(typeof promise.map).toBe('function')
224
+ expect(typeof promise.forEach).toBe('function')
225
+ expect(typeof promise.resolve).toBe('function')
226
+ expect(typeof promise.stream).toBe('function')
227
+ expect(typeof promise.addDependency).toBe('function')
228
+ })
229
+ })
230
+ })
231
+
232
+ // ============================================================================
233
+ // 3. Promise Interface (then/catch/finally)
234
+ // ============================================================================
235
+
236
+ describe('Promise Interface', () => {
237
+ describe('then()', () => {
238
+ it('returns a promise from then()', () => {
239
+ const promise = new AIPromise<string>('test')
240
+ const result = promise.then((val) => val)
241
+
242
+ expect(result).toBeInstanceOf(Promise)
243
+ })
244
+
245
+ it('chains multiple then() calls', () => {
246
+ const promise = new AIPromise<string>('test')
247
+ const result = promise.then((val) => val).then((val) => val.length)
248
+
249
+ expect(result).toBeInstanceOf(Promise)
250
+ })
251
+ })
252
+
253
+ describe('catch()', () => {
254
+ it('returns a promise from catch()', () => {
255
+ const promise = new AIPromise<string>('test')
256
+ const result = promise.catch((err) => 'fallback')
257
+
258
+ expect(result).toBeInstanceOf(Promise)
259
+ })
260
+ })
261
+
262
+ describe('finally()', () => {
263
+ it('returns a promise from finally()', () => {
264
+ const promise = new AIPromise<string>('test')
265
+ const result = promise.finally(() => {})
266
+
267
+ expect(result).toBeInstanceOf(Promise)
268
+ })
269
+
270
+ it('executes finally callback', async () => {
271
+ // This is a structural test - we just verify the API works
272
+ const promise = new AIPromise<string>('test')
273
+ let finallyCalled = false
274
+
275
+ const result = promise.finally(() => {
276
+ finallyCalled = true
277
+ })
278
+
279
+ expect(result).toBeInstanceOf(Promise)
280
+ })
281
+ })
282
+ })
283
+
284
+ // ============================================================================
285
+ // 4. Dependency Management
286
+ // ============================================================================
287
+
288
+ describe('Dependency Management', () => {
289
+ describe('addDependency()', () => {
290
+ it('adds a dependency to the promise', () => {
291
+ const promise1 = new AIPromise<string>('first prompt')
292
+ const promise2 = new AIPromise<string>('second prompt that uses ${dep_0}')
293
+
294
+ promise2.addDependency(promise1)
295
+
296
+ // Dependency is stored internally
297
+ const deps = (promise2 as any)._dependencies
298
+ expect(deps.length).toBe(1)
299
+ expect(deps[0].promise).toBe(promise1)
300
+ })
301
+
302
+ it('adds a dependency with a path', () => {
303
+ const promise1 = new AIPromise<{ name: string }>('first prompt')
304
+ const promise2 = new AIPromise<string>('second prompt')
305
+
306
+ promise2.addDependency(promise1, ['name'])
307
+
308
+ const deps = (promise2 as any)._dependencies
309
+ expect(deps.length).toBe(1)
310
+ expect(deps[0].path).toEqual(['name'])
311
+ })
312
+
313
+ it('supports multiple dependencies', () => {
314
+ const promise1 = new AIPromise<string>('first')
315
+ const promise2 = new AIPromise<string>('second')
316
+ const promise3 = new AIPromise<string>('third uses ${dep_0} and ${dep_1}')
317
+
318
+ promise3.addDependency(promise1)
319
+ promise3.addDependency(promise2)
320
+
321
+ const deps = (promise3 as any)._dependencies
322
+ expect(deps.length).toBe(2)
323
+ })
324
+ })
325
+ })
326
+
327
+ // ============================================================================
328
+ // 5. Factory Functions
329
+ // ============================================================================
330
+
331
+ describe('Factory Functions', () => {
332
+ describe('createTextPromise()', () => {
333
+ it('creates an AIPromise with text type', () => {
334
+ const promise = createTextPromise('write about TypeScript')
335
+
336
+ expect(isAIPromise(promise)).toBe(true)
337
+ expect(promise.prompt).toBe('write about TypeScript')
338
+ expect((promise as any)._options.type).toBe('text')
339
+ })
340
+
341
+ it('accepts options', () => {
342
+ const promise = createTextPromise('write', { model: 'sonnet' })
343
+
344
+ expect((promise as any)._options.model).toBe('sonnet')
345
+ })
346
+ })
347
+
348
+ describe('createObjectPromise()', () => {
349
+ it('creates an AIPromise with object type', () => {
350
+ const promise = createObjectPromise<{ name: string }>('generate a person')
351
+
352
+ expect(isAIPromise(promise)).toBe(true)
353
+ expect((promise as any)._options.type).toBe('object')
354
+ })
355
+ })
356
+
357
+ describe('createListPromise()', () => {
358
+ it('creates an AIPromise with list type', () => {
359
+ const promise = createListPromise('list 5 colors')
360
+
361
+ expect(isAIPromise(promise)).toBe(true)
362
+ expect((promise as any)._options.type).toBe('list')
363
+ })
364
+ })
365
+
366
+ describe('createListsPromise()', () => {
367
+ it('creates an AIPromise with lists type', () => {
368
+ const promise = createListsPromise('pros and cons of TypeScript')
369
+
370
+ expect(isAIPromise(promise)).toBe(true)
371
+ expect((promise as any)._options.type).toBe('lists')
372
+ })
373
+ })
374
+
375
+ describe('createBooleanPromise()', () => {
376
+ it('creates an AIPromise with boolean type', () => {
377
+ const promise = createBooleanPromise('is TypeScript better than JavaScript?')
378
+
379
+ expect(isAIPromise(promise)).toBe(true)
380
+ expect((promise as any)._options.type).toBe('boolean')
381
+ })
382
+ })
383
+
384
+ describe('createExtractPromise()', () => {
385
+ it('creates an AIPromise with extract type', () => {
386
+ const promise = createExtractPromise<string>('extract names from text')
387
+
388
+ expect(isAIPromise(promise)).toBe(true)
389
+ expect((promise as any)._options.type).toBe('extract')
390
+ })
391
+ })
392
+ })
393
+
394
+ // ============================================================================
395
+ // 6. Template Tag Helpers
396
+ // ============================================================================
397
+
398
+ describe('Template Tag Helpers', () => {
399
+ describe('parseTemplateWithDependencies()', () => {
400
+ it('parses a simple template without dependencies', () => {
401
+ const strings = ['Hello, world!'] as unknown as TemplateStringsArray
402
+ Object.defineProperty(strings, 'raw', { value: ['Hello, world!'] })
403
+
404
+ const result = parseTemplateWithDependencies(strings)
405
+
406
+ expect(result.prompt).toBe('Hello, world!')
407
+ expect(result.dependencies).toEqual([])
408
+ })
409
+
410
+ it('parses a template with string interpolation', () => {
411
+ const strings = ['Hello, ', '!'] as unknown as TemplateStringsArray
412
+ Object.defineProperty(strings, 'raw', { value: ['Hello, ', '!'] })
413
+
414
+ const result = parseTemplateWithDependencies(strings, 'World')
415
+
416
+ expect(result.prompt).toBe('Hello, World!')
417
+ expect(result.dependencies).toEqual([])
418
+ })
419
+
420
+ it('parses a template with AIPromise dependency', () => {
421
+ const strings = ['The topic is: ', ' - please expand'] as unknown as TemplateStringsArray
422
+ Object.defineProperty(strings, 'raw', { value: strings })
423
+
424
+ const aiPromise = new AIPromise<string>('generate a topic')
425
+ const result = parseTemplateWithDependencies(strings, aiPromise)
426
+
427
+ expect(result.prompt).toContain('${dep_0}')
428
+ expect(result.dependencies.length).toBe(1)
429
+ expect(result.dependencies[0].promise.prompt).toBe('generate a topic')
430
+ })
431
+
432
+ it('handles multiple AIPromise dependencies', () => {
433
+ const strings = [
434
+ 'Combine ',
435
+ ' with ',
436
+ ' to create something new',
437
+ ] as unknown as TemplateStringsArray
438
+ Object.defineProperty(strings, 'raw', { value: strings })
439
+
440
+ const promise1 = new AIPromise<string>('first topic')
441
+ const promise2 = new AIPromise<string>('second topic')
442
+ const result = parseTemplateWithDependencies(strings, promise1, promise2)
443
+
444
+ expect(result.dependencies.length).toBe(2)
445
+ expect(result.prompt).toContain('${dep_0}')
446
+ expect(result.prompt).toContain('${dep_1}')
447
+ })
448
+
449
+ it('handles mixed interpolation (strings and AIPromises)', () => {
450
+ const strings = [
451
+ 'Write about ',
452
+ ' by ',
453
+ ' in style of ',
454
+ '',
455
+ ] as unknown as TemplateStringsArray
456
+ Object.defineProperty(strings, 'raw', { value: strings })
457
+
458
+ const topicPromise = new AIPromise<string>('generate topic')
459
+ const result = parseTemplateWithDependencies(
460
+ strings,
461
+ topicPromise,
462
+ 'Author Name',
463
+ 'Hemingway'
464
+ )
465
+
466
+ expect(result.dependencies.length).toBe(1)
467
+ expect(result.prompt).toContain('Author Name')
468
+ expect(result.prompt).toContain('Hemingway')
469
+ })
470
+ })
471
+
472
+ describe('createAITemplateFunction()', () => {
473
+ it('creates a template function for text type', () => {
474
+ const templateFn = createAITemplateFunction<string>('text')
475
+
476
+ expect(typeof templateFn).toBe('function')
477
+ })
478
+
479
+ it('works as tagged template literal', () => {
480
+ const templateFn = createAITemplateFunction<string>('text')
481
+ const result = templateFn`Write about TypeScript`
482
+
483
+ expect(isAIPromise(result)).toBe(true)
484
+ })
485
+
486
+ it('works as regular function call', () => {
487
+ const templateFn = createAITemplateFunction<string>('text')
488
+ const result = templateFn('Write about TypeScript')
489
+
490
+ expect(isAIPromise(result)).toBe(true)
491
+ })
492
+
493
+ it('accepts options in function call mode', () => {
494
+ const templateFn = createAITemplateFunction<string>('text')
495
+ const result = templateFn('Write about TypeScript', { model: 'claude-opus-4-5' })
496
+
497
+ expect((result as any)._options.model).toBe('claude-opus-4-5')
498
+ })
499
+
500
+ it('tracks dependencies from template interpolation', () => {
501
+ const templateFn = createAITemplateFunction<string>('text')
502
+ const topicPromise = new AIPromise<string>('generate topic')
503
+
504
+ const result = templateFn`Write about ${topicPromise}`
505
+
506
+ const deps = (result as any)._dependencies
507
+ expect(deps.length).toBe(1)
508
+ })
509
+
510
+ it('creates boolean type promises', () => {
511
+ const isFn = createAITemplateFunction<boolean>('boolean')
512
+ const result = isFn`Is TypeScript better than JavaScript?`
513
+
514
+ expect((result as any)._options.type).toBe('boolean')
515
+ })
516
+
517
+ it('creates list type promises', () => {
518
+ const listFn = createAITemplateFunction<string[]>('list')
519
+ const result = listFn`5 programming languages`
520
+
521
+ expect((result as any)._options.type).toBe('list')
522
+ })
523
+ })
524
+ })
525
+
526
+ // ============================================================================
527
+ // 7. Map Operations
528
+ // ============================================================================
529
+
530
+ describe('Map Operations', () => {
531
+ describe('map()', () => {
532
+ it('returns a BatchMapPromise', () => {
533
+ const promise = new AIPromise<string[]>('list items', { type: 'list' })
534
+ const mapped = promise.map((item) => `processed: ${item}`)
535
+
536
+ expect(isBatchMapPromise(mapped)).toBe(true)
537
+ })
538
+
539
+ it('map() method exists on list promises', () => {
540
+ const promise = createListPromise('list 5 colors')
541
+
542
+ expect(typeof promise.map).toBe('function')
543
+ })
544
+ })
545
+
546
+ describe('mapImmediate()', () => {
547
+ it('returns a BatchMapPromise with immediate option', () => {
548
+ const promise = new AIPromise<string[]>('list items', { type: 'list' })
549
+ const mapped = promise.mapImmediate((item) => `processed: ${item}`)
550
+
551
+ expect(isBatchMapPromise(mapped)).toBe(true)
552
+ expect((mapped as any)._options.immediate).toBe(true)
553
+ })
554
+ })
555
+
556
+ describe('mapDeferred()', () => {
557
+ it('returns a BatchMapPromise with deferred option', () => {
558
+ const promise = new AIPromise<string[]>('list items', { type: 'list' })
559
+ const mapped = promise.mapDeferred((item) => `processed: ${item}`)
560
+
561
+ expect(isBatchMapPromise(mapped)).toBe(true)
562
+ expect((mapped as any)._options.deferred).toBe(true)
563
+ })
564
+ })
565
+ })
566
+
567
+ // ============================================================================
568
+ // 8. forEach Operations
569
+ // ============================================================================
570
+
571
+ describe('forEach Operations', () => {
572
+ describe('forEach()', () => {
573
+ it('forEach method exists on AIPromise', () => {
574
+ const promise = new AIPromise<string[]>('list items', { type: 'list' })
575
+
576
+ expect(typeof promise.forEach).toBe('function')
577
+ })
578
+ })
579
+ })
580
+
581
+ // ============================================================================
582
+ // 9. AsyncIterator Support
583
+ // ============================================================================
584
+
585
+ describe('AsyncIterator Support', () => {
586
+ it('AIPromise has Symbol.asyncIterator', () => {
587
+ const promise = new AIPromise<string[]>('list items', { type: 'list' })
588
+
589
+ expect(typeof promise[Symbol.asyncIterator]).toBe('function')
590
+ })
591
+ })
592
+
593
+ // ============================================================================
594
+ // 10. Streaming Interface
595
+ // ============================================================================
596
+
597
+ describe('Streaming Interface', () => {
598
+ describe('stream()', () => {
599
+ it('returns a StreamingAIPromise', () => {
600
+ const promise = new AIPromise<string>('test prompt', { type: 'text' })
601
+ const stream = promise.stream()
602
+
603
+ expect(stream).toBeDefined()
604
+ expect(typeof stream[Symbol.asyncIterator]).toBe('function')
605
+ })
606
+
607
+ it('stream has textStream property', () => {
608
+ const promise = new AIPromise<string>('test prompt', { type: 'text' })
609
+ const stream = promise.stream()
610
+
611
+ expect(stream.textStream).toBeDefined()
612
+ expect(typeof stream.textStream[Symbol.asyncIterator]).toBe('function')
613
+ })
614
+
615
+ it('stream has partialObjectStream property', () => {
616
+ const promise = new AIPromise<{ name: string }>('test prompt', { type: 'object' })
617
+ const stream = promise.stream()
618
+
619
+ expect(stream.partialObjectStream).toBeDefined()
620
+ expect(typeof stream.partialObjectStream[Symbol.asyncIterator]).toBe('function')
621
+ })
622
+
623
+ it('stream has result promise', () => {
624
+ const promise = new AIPromise<string>('test prompt', { type: 'text' })
625
+ const stream = promise.stream()
626
+
627
+ expect(stream.result).toBeDefined()
628
+ expect(typeof stream.result.then).toBe('function')
629
+ })
630
+
631
+ it('stream is thenable', () => {
632
+ const promise = new AIPromise<string>('test prompt', { type: 'text' })
633
+ const stream = promise.stream()
634
+
635
+ expect(typeof stream.then).toBe('function')
636
+ })
637
+
638
+ it('accepts abort signal option', () => {
639
+ const promise = new AIPromise<string>('test prompt', { type: 'text' })
640
+ const controller = new AbortController()
641
+ const stream = promise.stream({ abortSignal: controller.signal })
642
+
643
+ expect(stream).toBeDefined()
644
+ })
645
+ })
646
+ })
647
+
648
+ // ============================================================================
649
+ // 11. Batch Map Integration
650
+ // ============================================================================
651
+
652
+ describe('Batch Map Integration', () => {
653
+ describe('BatchMapPromise', () => {
654
+ it('can be created with items and operations', () => {
655
+ const batchMap = new BatchMapPromise<string>(['a', 'b', 'c'], [], {})
656
+
657
+ expect(batchMap.size).toBe(3)
658
+ })
659
+
660
+ it('has BATCH_MAP_SYMBOL', () => {
661
+ const batchMap = new BatchMapPromise<string>([], [], {})
662
+
663
+ expect((batchMap as any)[BATCH_MAP_SYMBOL]).toBe(true)
664
+ })
665
+
666
+ it('isBatchMapPromise returns true for BatchMapPromise', () => {
667
+ const batchMap = new BatchMapPromise<string>([], [], {})
668
+
669
+ expect(isBatchMapPromise(batchMap)).toBe(true)
670
+ })
671
+
672
+ it('isBatchMapPromise returns false for regular promises', () => {
673
+ expect(isBatchMapPromise(Promise.resolve([]))).toBe(false)
674
+ })
675
+
676
+ it('isBatchMapPromise returns false for AIPromise', () => {
677
+ const promise = new AIPromise<string[]>('test', { type: 'list' })
678
+
679
+ expect(isBatchMapPromise(promise)).toBe(false)
680
+ })
681
+ })
682
+
683
+ describe('Recording Mode', () => {
684
+ it('isInRecordingMode returns false outside of batch map', () => {
685
+ expect(isInRecordingMode()).toBe(false)
686
+ })
687
+
688
+ it('getCurrentItemPlaceholder returns null outside of batch map', () => {
689
+ expect(getCurrentItemPlaceholder()).toBe(null)
690
+ })
691
+ })
692
+
693
+ describe('createBatchMap()', () => {
694
+ it('creates a BatchMapPromise from items and callback', () => {
695
+ const items = ['a', 'b', 'c']
696
+ const callback = (item: string) => item.toUpperCase()
697
+
698
+ const batchMap = createBatchMap(items, callback)
699
+
700
+ expect(isBatchMapPromise(batchMap)).toBe(true)
701
+ expect(batchMap.size).toBe(3)
702
+ })
703
+
704
+ it('accepts options', () => {
705
+ const items = ['a', 'b']
706
+ const callback = (item: string) => item
707
+
708
+ const batchMap = createBatchMap(items, callback, { immediate: true })
709
+
710
+ expect((batchMap as any)._options.immediate).toBe(true)
711
+ })
712
+ })
713
+ })
714
+
715
+ // ============================================================================
716
+ // 12. Schema Building
717
+ // ============================================================================
718
+
719
+ describe('Schema Building', () => {
720
+ describe('_buildSchema() internal method', () => {
721
+ it('uses base schema when no properties accessed', () => {
722
+ const promise = new AIPromise<{ name: string }>('test', {
723
+ baseSchema: { name: 'The person name' },
724
+ })
725
+
726
+ const schema = (promise as any)._buildSchema()
727
+ expect(schema).toEqual({ name: 'The person name' })
728
+ })
729
+
730
+ it('infers list type from property name ending in s', () => {
731
+ const promise = new AIPromise<{ items: string[] }>('test')
732
+
733
+ // Access the property to track it
734
+ const { items } = promise as any
735
+
736
+ const schema = (promise as any)._buildSchema()
737
+ expect(Array.isArray(schema.items)).toBe(true)
738
+ })
739
+
740
+ it('infers boolean type from property name patterns', () => {
741
+ const promise = new AIPromise<{ isValid: boolean; hasError: boolean }>('test')
742
+
743
+ // Access the properties
744
+ const { isValid, hasError } = promise as any
745
+
746
+ const schema = (promise as any)._buildSchema()
747
+ expect(schema.isValid).toContain('true/false')
748
+ expect(schema.hasError).toContain('true/false')
749
+ })
750
+
751
+ it('infers number type from property name patterns', () => {
752
+ const promise = new AIPromise<{ count: number; totalAmount: number }>('test')
753
+
754
+ // Access the properties
755
+ const { count, totalAmount } = promise as any
756
+
757
+ const schema = (promise as any)._buildSchema()
758
+ expect(schema.count).toContain('number')
759
+ expect(schema.totalAmount).toContain('number')
760
+ })
761
+
762
+ it('returns default schema for text type', () => {
763
+ const promise = new AIPromise<string>('test', { type: 'text' })
764
+
765
+ const schema = (promise as any)._buildSchema()
766
+ expect(schema).toHaveProperty('text')
767
+ })
768
+
769
+ it('returns default schema for list type', () => {
770
+ const promise = new AIPromise<string[]>('test', { type: 'list' })
771
+
772
+ const schema = (promise as any)._buildSchema()
773
+ expect(schema).toHaveProperty('items')
774
+ expect(Array.isArray(schema.items)).toBe(true)
775
+ })
776
+
777
+ it('returns default schema for boolean type', () => {
778
+ const promise = new AIPromise<boolean>('test', { type: 'boolean' })
779
+
780
+ const schema = (promise as any)._buildSchema()
781
+ expect(schema).toHaveProperty('answer')
782
+ })
783
+
784
+ it('returns default schema for extract type', () => {
785
+ const promise = new AIPromise<string[]>('test', { type: 'extract' })
786
+
787
+ const schema = (promise as any)._buildSchema()
788
+ expect(schema).toHaveProperty('items')
789
+ })
790
+
791
+ it('returns default schema for lists type', () => {
792
+ const promise = new AIPromise<Record<string, string[]>>('test', { type: 'lists' })
793
+
794
+ const schema = (promise as any)._buildSchema()
795
+ expect(schema).toHaveProperty('categories')
796
+ expect(schema).toHaveProperty('data')
797
+ })
798
+ })
799
+ })
800
+
801
+ // ============================================================================
802
+ // 13. Error Handling
803
+ // ============================================================================
804
+
805
+ describe('Error Handling', () => {
806
+ describe('proxy errors', () => {
807
+ it('throws on set attempt', () => {
808
+ const promise = new AIPromise<{ name: string }>('test')
809
+
810
+ expect(() => {
811
+ ;(promise as any).name = 'value'
812
+ }).toThrow('read-only')
813
+ })
814
+
815
+ it('throws on delete attempt', () => {
816
+ const promise = new AIPromise<{ name: string }>('test')
817
+
818
+ expect(() => {
819
+ delete (promise as any).name
820
+ }).toThrow('cannot be deleted')
821
+ })
822
+ })
823
+
824
+ describe('resolution errors', () => {
825
+ it('catch catches rejection from promise chain', async () => {
826
+ const promise = new AIPromise<string>('test')
827
+
828
+ // Test that catch method works correctly by chaining
829
+ const result = promise
830
+ .then(() => {
831
+ throw new Error('Test error')
832
+ })
833
+ .catch((err) => 'caught')
834
+
835
+ // This tests the catch method behavior - it should handle the thrown error
836
+ expect(result).toBeInstanceOf(Promise)
837
+ })
838
+
839
+ it('finally is called regardless of resolution', async () => {
840
+ const promise = new AIPromise<string>('test')
841
+
842
+ let finallyCalled = false
843
+ const result = promise.finally(() => {
844
+ finallyCalled = true
845
+ })
846
+
847
+ // finally() should return a promise
848
+ expect(result).toBeInstanceOf(Promise)
849
+ })
850
+ })
851
+ })
852
+
853
+ // ============================================================================
854
+ // 14. Parent-Child Promise Relationships
855
+ // ============================================================================
856
+
857
+ describe('Parent-Child Promise Relationships', () => {
858
+ it('child promise has reference to parent', () => {
859
+ const parent = new AIPromise<{ name: string }>('test parent prompt')
860
+ const child = (parent as any).name
861
+
862
+ // Get the raw promise to access _parent
863
+ const rawChild = getRawPromise(child)
864
+ const rawParent = getRawPromise(parent)
865
+
866
+ // Check that the child's parent has the same prompt as the parent
867
+ expect((rawChild as any)._parent?.prompt).toBe('test parent prompt')
868
+ })
869
+
870
+ it('child promise has correct property path', () => {
871
+ const parent = new AIPromise<{ user: { name: string } }>('test')
872
+ const name = (parent as any).user.name
873
+
874
+ expect(name.path).toEqual(['user', 'name'])
875
+ })
876
+
877
+ it('deeply nested access builds complete path', () => {
878
+ const promise = new AIPromise<{ a: { b: { c: { d: string } } } }>('test')
879
+ const deep = (promise as any).a.b.c.d
880
+
881
+ expect(deep.path).toEqual(['a', 'b', 'c', 'd'])
882
+ })
883
+ })
884
+
885
+ // ============================================================================
886
+ // 15. Promise Pipelining (without await)
887
+ // ============================================================================
888
+
889
+ describe('Promise Pipelining', () => {
890
+ it('allows chaining without await', () => {
891
+ // Create a chain of promises
892
+ const topic = new AIPromise<string>('generate a topic', { type: 'text' })
893
+ const essay = new AIPromise<string>('write about the topic', { type: 'text' })
894
+
895
+ // Add dependency
896
+ essay.addDependency(topic)
897
+
898
+ // The chain exists without any await
899
+ expect(isAIPromise(topic)).toBe(true)
900
+ expect(isAIPromise(essay)).toBe(true)
901
+ expect((essay as any)._dependencies.length).toBe(1)
902
+ })
903
+
904
+ it('property access creates dependent promises', () => {
905
+ const promise = new AIPromise<{ title: string; body: string }>('generate article')
906
+
907
+ // Access properties - creates child promises
908
+ const { title, body } = promise as any
909
+
910
+ // Both are AIPromises
911
+ expect(isAIPromise(title)).toBe(true)
912
+ expect(isAIPromise(body)).toBe(true)
913
+
914
+ // Both have parent reference
915
+ expect((title as any)._parent.prompt).toBe('generate article')
916
+ expect((body as any)._parent.prompt).toBe('generate article')
917
+ })
918
+ })
919
+
920
+ // ============================================================================
921
+ // 16. Type-Specific Behavior Tests
922
+ // ============================================================================
923
+
924
+ describe('Type-Specific Behavior', () => {
925
+ describe('text type', () => {
926
+ it('creates promise with text type', () => {
927
+ const promise = createTextPromise('write a haiku')
928
+ expect((promise as any)._options.type).toBe('text')
929
+ })
930
+ })
931
+
932
+ describe('object type', () => {
933
+ it('creates promise with object type', () => {
934
+ const promise = createObjectPromise('generate person data')
935
+ expect((promise as any)._options.type).toBe('object')
936
+ })
937
+
938
+ it('tracks accessed properties for schema', () => {
939
+ const promise = createObjectPromise<{ name: string; age: number }>('person')
940
+ const { name, age } = promise as any
941
+
942
+ expect(promise.accessedProps.has('name')).toBe(true)
943
+ expect(promise.accessedProps.has('age')).toBe(true)
944
+ })
945
+ })
946
+
947
+ describe('list type', () => {
948
+ it('creates promise with list type', () => {
949
+ const promise = createListPromise('5 colors')
950
+ expect((promise as any)._options.type).toBe('list')
951
+ })
952
+
953
+ it('supports map operations', () => {
954
+ const promise = createListPromise('3 numbers')
955
+ expect(typeof promise.map).toBe('function')
956
+ })
957
+ })
958
+
959
+ describe('boolean type', () => {
960
+ it('creates promise with boolean type', () => {
961
+ const promise = createBooleanPromise('is it true?')
962
+ expect((promise as any)._options.type).toBe('boolean')
963
+ })
964
+ })
965
+
966
+ describe('extract type', () => {
967
+ it('creates promise with extract type', () => {
968
+ const promise = createExtractPromise('extract names')
969
+ expect((promise as any)._options.type).toBe('extract')
970
+ })
971
+ })
972
+
973
+ describe('lists type', () => {
974
+ it('creates promise with lists type', () => {
975
+ const promise = createListsPromise('pros and cons')
976
+ expect((promise as any)._options.type).toBe('lists')
977
+ })
978
+ })
979
+ })
980
+
981
+ // ============================================================================
982
+ // 17. Resolution State Management
983
+ // ============================================================================
984
+
985
+ describe('Resolution State Management', () => {
986
+ it('starts unresolved', () => {
987
+ const promise = new AIPromise<string>('test')
988
+ expect(promise.isResolved).toBe(false)
989
+ })
990
+
991
+ it('caches resolver promise', () => {
992
+ const promise = new AIPromise<string>('test')
993
+
994
+ // Call then twice
995
+ const result1 = promise.then((v) => v)
996
+ const result2 = promise.then((v) => v)
997
+
998
+ // Both should use the same resolver
999
+ // (We can't directly test the internal _resolver, but the behavior should be consistent)
1000
+ expect(result1).toBeInstanceOf(Promise)
1001
+ expect(result2).toBeInstanceOf(Promise)
1002
+ })
1003
+ })
1004
+
1005
+ // ============================================================================
1006
+ // Integration Tests (Real AI calls)
1007
+ // ============================================================================
1008
+
1009
+ describe.skipIf(!hasGateway)('AIPromise Integration Tests', () => {
1010
+ describe('Basic Resolution', () => {
1011
+ it('resolves a text promise', async () => {
1012
+ const promise = createTextPromise('Say hello in exactly 2 words')
1013
+ const result = await promise
1014
+
1015
+ expect(typeof result).toBe('string')
1016
+ expect(result.length).toBeGreaterThan(0)
1017
+ }, 60000)
1018
+
1019
+ it('resolves an object promise with accessed properties', async () => {
1020
+ const promise = createObjectPromise<{ greeting: string; language: string }>(
1021
+ 'Generate a greeting in French'
1022
+ )
1023
+
1024
+ // Access properties to define schema
1025
+ const { greeting, language } = promise as any
1026
+
1027
+ const result = await promise
1028
+
1029
+ expect(result).toHaveProperty('greeting')
1030
+ expect(result).toHaveProperty('language')
1031
+ }, 60000)
1032
+
1033
+ it('resolves a list promise', async () => {
1034
+ const promise = createListPromise('List exactly 3 primary colors')
1035
+ const result = await promise
1036
+
1037
+ expect(Array.isArray(result)).toBe(true)
1038
+ expect(result.length).toBeGreaterThanOrEqual(3)
1039
+ }, 60000)
1040
+
1041
+ it('resolves a boolean promise', async () => {
1042
+ const promise = createBooleanPromise('Is the sky blue on a clear day?')
1043
+ const result = await promise
1044
+
1045
+ expect(typeof result).toBe('boolean')
1046
+ expect(result).toBe(true)
1047
+ }, 60000)
1048
+ })
1049
+
1050
+ describe('Property Access Resolution', () => {
1051
+ it('resolves child property from parent', async () => {
1052
+ const promise = createObjectPromise<{ name: string; age: number }>(
1053
+ 'Generate a fictional person with name John and age 30'
1054
+ )
1055
+
1056
+ // Access properties
1057
+ const { name } = promise as any
1058
+
1059
+ // Resolve the parent first
1060
+ const parent = await promise
1061
+
1062
+ // The child should resolve to the parent's property
1063
+ expect(parent.name).toBeDefined()
1064
+ }, 60000)
1065
+ })
1066
+
1067
+ describe('Dependency Resolution', () => {
1068
+ it('resolves dependencies before main promise', async () => {
1069
+ const topicPromise = createTextPromise('Pick a topic: science or art. Just say one word.')
1070
+ const essayPromise = createTextPromise('Write one sentence about ${dep_0}')
1071
+
1072
+ essayPromise.addDependency(topicPromise)
1073
+
1074
+ const result = await essayPromise
1075
+
1076
+ expect(typeof result).toBe('string')
1077
+ expect(result.length).toBeGreaterThan(0)
1078
+ }, 120000)
1079
+ })
1080
+ })