ai-functions 2.1.3 → 2.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 (284) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +90 -1
  3. package/README.md +38 -0
  4. package/dist/ai-promise.d.ts +3 -3
  5. package/dist/ai-promise.d.ts.map +1 -1
  6. package/dist/ai-promise.js +135 -64
  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 +51 -858
  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.map +1 -1
  56. package/dist/budget.js +27 -14
  57. package/dist/budget.js.map +1 -1
  58. package/dist/cache.d.ts +23 -0
  59. package/dist/cache.d.ts.map +1 -1
  60. package/dist/cache.js +36 -15
  61. package/dist/cache.js.map +1 -1
  62. package/dist/context.d.ts +26 -8
  63. package/dist/context.d.ts.map +1 -1
  64. package/dist/context.js +64 -62
  65. package/dist/context.js.map +1 -1
  66. package/dist/digital-objects-registry.d.ts +229 -0
  67. package/dist/digital-objects-registry.d.ts.map +1 -0
  68. package/dist/digital-objects-registry.js +617 -0
  69. package/dist/digital-objects-registry.js.map +1 -0
  70. package/dist/embeddings.d.ts +2 -2
  71. package/dist/embeddings.d.ts.map +1 -1
  72. package/dist/errors.d.ts +22 -0
  73. package/dist/errors.d.ts.map +1 -0
  74. package/dist/errors.js +35 -0
  75. package/dist/errors.js.map +1 -0
  76. package/dist/eval/runner.d.ts +8 -0
  77. package/dist/eval/runner.d.ts.map +1 -1
  78. package/dist/eval/runner.js +41 -35
  79. package/dist/eval/runner.js.map +1 -1
  80. package/dist/eval-log/in-memory.d.ts +34 -0
  81. package/dist/eval-log/in-memory.d.ts.map +1 -0
  82. package/dist/eval-log/in-memory.js +84 -0
  83. package/dist/eval-log/in-memory.js.map +1 -0
  84. package/dist/eval-log/index.d.ts +29 -0
  85. package/dist/eval-log/index.d.ts.map +1 -0
  86. package/dist/eval-log/index.js +39 -0
  87. package/dist/eval-log/index.js.map +1 -0
  88. package/dist/eval-log/types.d.ts +101 -0
  89. package/dist/eval-log/types.d.ts.map +1 -0
  90. package/dist/eval-log/types.js +16 -0
  91. package/dist/eval-log/types.js.map +1 -0
  92. package/dist/function-registry.d.ts +176 -0
  93. package/dist/function-registry.d.ts.map +1 -0
  94. package/dist/function-registry.js +685 -0
  95. package/dist/function-registry.js.map +1 -0
  96. package/dist/generate.d.ts +9 -3
  97. package/dist/generate.d.ts.map +1 -1
  98. package/dist/generate.js +18 -18
  99. package/dist/generate.js.map +1 -1
  100. package/dist/index.d.ts +18 -11
  101. package/dist/index.d.ts.map +1 -1
  102. package/dist/index.js +35 -18
  103. package/dist/index.js.map +1 -1
  104. package/dist/logger.d.ts +118 -0
  105. package/dist/logger.d.ts.map +1 -0
  106. package/dist/logger.js +187 -0
  107. package/dist/logger.js.map +1 -0
  108. package/dist/middleware/budget.d.ts +84 -0
  109. package/dist/middleware/budget.d.ts.map +1 -0
  110. package/dist/middleware/budget.js +110 -0
  111. package/dist/middleware/budget.js.map +1 -0
  112. package/dist/middleware/cache.d.ts +103 -0
  113. package/dist/middleware/cache.d.ts.map +1 -0
  114. package/dist/middleware/cache.js +228 -0
  115. package/dist/middleware/cache.js.map +1 -0
  116. package/dist/middleware/embed-cache.d.ts +99 -0
  117. package/dist/middleware/embed-cache.d.ts.map +1 -0
  118. package/dist/middleware/embed-cache.js +128 -0
  119. package/dist/middleware/embed-cache.js.map +1 -0
  120. package/dist/middleware/index.d.ts +11 -0
  121. package/dist/middleware/index.d.ts.map +1 -0
  122. package/dist/middleware/index.js +11 -0
  123. package/dist/middleware/index.js.map +1 -0
  124. package/dist/middleware/trace.d.ts +103 -0
  125. package/dist/middleware/trace.d.ts.map +1 -0
  126. package/dist/middleware/trace.js +176 -0
  127. package/dist/middleware/trace.js.map +1 -0
  128. package/dist/primitives.d.ts +120 -1
  129. package/dist/primitives.d.ts.map +1 -1
  130. package/dist/primitives.js +398 -26
  131. package/dist/primitives.js.map +1 -1
  132. package/dist/retry.d.ts +66 -1
  133. package/dist/retry.d.ts.map +1 -1
  134. package/dist/retry.js +115 -8
  135. package/dist/retry.js.map +1 -1
  136. package/dist/sandbox.d.ts +36 -0
  137. package/dist/sandbox.d.ts.map +1 -0
  138. package/dist/sandbox.js +44 -0
  139. package/dist/sandbox.js.map +1 -0
  140. package/dist/schema.js +2 -2
  141. package/dist/schema.js.map +1 -1
  142. package/dist/telemetry.d.ts +128 -0
  143. package/dist/telemetry.d.ts.map +1 -0
  144. package/dist/telemetry.js +285 -0
  145. package/dist/telemetry.js.map +1 -0
  146. package/dist/template.d.ts.map +1 -1
  147. package/dist/template.js +6 -1
  148. package/dist/template.js.map +1 -1
  149. package/dist/tool-orchestration.d.ts +66 -4
  150. package/dist/tool-orchestration.d.ts.map +1 -1
  151. package/dist/tool-orchestration.js +123 -23
  152. package/dist/tool-orchestration.js.map +1 -1
  153. package/dist/type-guards.d.ts +28 -0
  154. package/dist/type-guards.d.ts.map +1 -0
  155. package/dist/type-guards.js +29 -0
  156. package/dist/type-guards.js.map +1 -0
  157. package/dist/types.d.ts +155 -19
  158. package/dist/types.d.ts.map +1 -1
  159. package/dist/types.js +36 -1
  160. package/dist/types.js.map +1 -1
  161. package/dist/wrap-for-v3.d.ts +80 -0
  162. package/dist/wrap-for-v3.d.ts.map +1 -0
  163. package/dist/wrap-for-v3.js +89 -0
  164. package/dist/wrap-for-v3.js.map +1 -0
  165. package/examples/00-quickstart.ts +232 -0
  166. package/examples/01-rag-chatbot.ts +212 -0
  167. package/examples/02-multi-agent-research.ts +290 -0
  168. package/examples/03-email-classification.ts +379 -0
  169. package/examples/04-content-moderation.ts +400 -0
  170. package/examples/05-document-extraction.ts +455 -0
  171. package/examples/06-streaming-chat-nextjs.ts +437 -0
  172. package/examples/07-cloudflare-worker.ts +483 -0
  173. package/examples/08-batch-processing.ts +491 -0
  174. package/examples/09-budget-constrained.ts +527 -0
  175. package/examples/10-tool-orchestration.ts +565 -0
  176. package/examples/11-retry-resilience.ts +403 -0
  177. package/examples/12-caching-strategies.ts +422 -0
  178. package/examples/README.md +145 -0
  179. package/package.json +29 -25
  180. package/src/ai-promise.ts +226 -140
  181. package/src/ai-schemas.ts +122 -0
  182. package/src/ai.ts +71 -1176
  183. package/src/batch/anthropic.ts +96 -161
  184. package/src/batch/bedrock.ts +203 -454
  185. package/src/batch/cloudflare.ts +99 -282
  186. package/src/batch/google.ts +91 -297
  187. package/src/batch/index.ts +4 -1
  188. package/src/batch/memory.ts +15 -10
  189. package/src/batch/openai.ts +65 -193
  190. package/src/batch/provider.ts +336 -0
  191. package/src/batch-map.ts +29 -24
  192. package/src/batch-queue.ts +200 -11
  193. package/src/budget.ts +31 -18
  194. package/src/cache.ts +45 -17
  195. package/src/context.ts +106 -77
  196. package/src/digital-objects-registry.ts +750 -0
  197. package/src/errors.ts +37 -0
  198. package/src/eval/runner.ts +60 -36
  199. package/src/eval-log/in-memory.ts +90 -0
  200. package/src/eval-log/index.ts +46 -0
  201. package/src/eval-log/types.ts +110 -0
  202. package/src/function-registry.ts +874 -0
  203. package/src/generate.ts +33 -28
  204. package/src/index.ts +122 -21
  205. package/src/logger.ts +232 -0
  206. package/src/middleware/budget.ts +171 -0
  207. package/src/middleware/cache.ts +299 -0
  208. package/src/middleware/embed-cache.ts +195 -0
  209. package/src/middleware/index.ts +23 -0
  210. package/src/middleware/trace.ts +248 -0
  211. package/src/primitives.ts +589 -62
  212. package/src/retry.ts +144 -18
  213. package/src/sandbox.ts +52 -0
  214. package/src/schema.ts +8 -8
  215. package/src/telemetry.ts +403 -0
  216. package/src/template.ts +8 -4
  217. package/src/tool-orchestration.ts +213 -48
  218. package/src/type-guards.ts +31 -0
  219. package/src/types.ts +186 -27
  220. package/src/wrap-for-v3.ts +105 -0
  221. package/test/ai-promise.test.ts +1080 -0
  222. package/test/ai-proxy.test.ts +1 -1
  223. package/test/batch-autosubmit-errors.test.ts +49 -37
  224. package/test/batch-blog-posts.test.ts +87 -129
  225. package/test/core-functions.test.ts +183 -579
  226. package/test/decide.test.ts +154 -322
  227. package/test/define.test.ts +211 -8
  228. package/test/digital-objects-registry.test.ts +760 -0
  229. package/test/embedding-cache-middleware.test.ts +140 -0
  230. package/test/fill-template.test.ts +89 -0
  231. package/test/generate-core.test.ts +140 -229
  232. package/test/implicit-batch.test.ts +22 -65
  233. package/test/retry-policy-integration.test.ts +117 -0
  234. package/test/sandbox-execution.test.ts +155 -0
  235. package/test/schema.test.ts +55 -19
  236. package/test/template.test.ts +1164 -0
  237. package/test/tool-orchestration.test.ts +270 -0
  238. package/test/wrap-for-v3.test.ts +612 -0
  239. package/vitest.config.js +6 -0
  240. package/vitest.config.ts +20 -0
  241. package/LICENSE +0 -21
  242. package/dist/rpc/auth.d.ts +0 -69
  243. package/dist/rpc/auth.d.ts.map +0 -1
  244. package/dist/rpc/auth.js +0 -136
  245. package/dist/rpc/auth.js.map +0 -1
  246. package/dist/rpc/client.d.ts +0 -62
  247. package/dist/rpc/client.d.ts.map +0 -1
  248. package/dist/rpc/client.js +0 -103
  249. package/dist/rpc/client.js.map +0 -1
  250. package/dist/rpc/deferred.d.ts +0 -60
  251. package/dist/rpc/deferred.d.ts.map +0 -1
  252. package/dist/rpc/deferred.js +0 -96
  253. package/dist/rpc/deferred.js.map +0 -1
  254. package/dist/rpc/index.d.ts +0 -22
  255. package/dist/rpc/index.d.ts.map +0 -1
  256. package/dist/rpc/index.js +0 -38
  257. package/dist/rpc/index.js.map +0 -1
  258. package/dist/rpc/local.d.ts +0 -42
  259. package/dist/rpc/local.d.ts.map +0 -1
  260. package/dist/rpc/local.js +0 -50
  261. package/dist/rpc/local.js.map +0 -1
  262. package/dist/rpc/server.d.ts +0 -165
  263. package/dist/rpc/server.d.ts.map +0 -1
  264. package/dist/rpc/server.js +0 -405
  265. package/dist/rpc/server.js.map +0 -1
  266. package/dist/rpc/session.d.ts +0 -32
  267. package/dist/rpc/session.d.ts.map +0 -1
  268. package/dist/rpc/session.js +0 -43
  269. package/dist/rpc/session.js.map +0 -1
  270. package/dist/rpc/transport.d.ts +0 -306
  271. package/dist/rpc/transport.d.ts.map +0 -1
  272. package/dist/rpc/transport.js +0 -731
  273. package/dist/rpc/transport.js.map +0 -1
  274. package/src/batch/anthropic.js +0 -256
  275. package/src/batch/bedrock.js +0 -584
  276. package/src/batch/cloudflare.js +0 -287
  277. package/src/batch/google.js +0 -359
  278. package/src/batch/index.js +0 -30
  279. package/src/batch/memory.js +0 -187
  280. package/src/batch/openai.js +0 -402
  281. package/src/eval/index.js +0 -7
  282. package/src/eval/models.js +0 -119
  283. package/src/eval/runner.js +0 -147
  284. package/test/schema.test.js +0 -96
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Integration tests: retry/CB/fallback machinery reads policy data from
3
+ * `language-models`. Validates that the per-model `ModelPolicy` flows through
4
+ * the resilience classes correctly.
5
+ */
6
+
7
+ import { describe, it, expect, vi } from 'vitest'
8
+ import { RetryPolicy, CircuitBreaker, FallbackChain } from '../src/retry.js'
9
+ import { tiersForModel, modelSupportsTier, modelPolicyFor } from '../src/index.js'
10
+
11
+ describe('RetryPolicy.forModel', () => {
12
+ it('uses frontier-provider settings for sonnet', async () => {
13
+ const policy = RetryPolicy.forModel('sonnet')
14
+ let attempts = 0
15
+ const op = vi.fn(async () => {
16
+ attempts++
17
+ if (attempts < 2) {
18
+ const err = new Error('rate limit')
19
+ ;(err as Error & { status?: number }).status = 429
20
+ throw err
21
+ }
22
+ return 'ok'
23
+ })
24
+ const result = await policy.execute(() => op())
25
+ expect(result).toBe('ok')
26
+ expect(attempts).toBe(2)
27
+ })
28
+
29
+ it('forModel respects per-call overrides', async () => {
30
+ const policy = RetryPolicy.forModel('sonnet', { maxRetries: 0 })
31
+ let attempts = 0
32
+ const op = async () => {
33
+ attempts++
34
+ const err = new Error('rate limit')
35
+ ;(err as Error & { status?: number }).status = 429
36
+ throw err
37
+ }
38
+ await expect(policy.execute(() => op())).rejects.toThrow()
39
+ expect(attempts).toBe(1) // No retries
40
+ })
41
+ })
42
+
43
+ describe('CircuitBreaker.forModel', () => {
44
+ it('uses frontier-provider failure threshold for sonnet', async () => {
45
+ const breaker = CircuitBreaker.forModel('sonnet')
46
+ // Frontier threshold is 8 — eight failures should still report state
47
+ // before opening on the 8th.
48
+ for (let i = 0; i < 7; i++) {
49
+ try {
50
+ await breaker.execute(async () => {
51
+ throw new Error('fail')
52
+ })
53
+ } catch {
54
+ // expected
55
+ }
56
+ }
57
+ // Still closed (threshold is 8)
58
+ expect(breaker.state).toBe('closed')
59
+ })
60
+
61
+ it('forModel respects per-call overrides', async () => {
62
+ const breaker = CircuitBreaker.forModel('sonnet', { failureThreshold: 1 })
63
+ try {
64
+ await breaker.execute(async () => {
65
+ throw new Error('fail')
66
+ })
67
+ } catch {
68
+ // expected
69
+ }
70
+ expect(breaker.state).toBe('open')
71
+ })
72
+ })
73
+
74
+ describe('FallbackChain.forModel', () => {
75
+ it('builds a chain from policy.fallbackChain plus the primary model id', async () => {
76
+ const seen: string[] = []
77
+ const chain = FallbackChain.forModel<string, void>('sonnet', async (modelId) => {
78
+ seen.push(modelId)
79
+ throw new Error('fail')
80
+ })
81
+ await expect(chain.execute()).rejects.toThrow('All fallback models failed')
82
+ // First call is the primary alias, rest are fallbacks
83
+ expect(seen[0]).toBe('anthropic/claude-sonnet-4.5')
84
+ expect(seen.length).toBeGreaterThan(1)
85
+ })
86
+
87
+ it('returns first successful model result', async () => {
88
+ let calls = 0
89
+ const chain = FallbackChain.forModel<string, void>('sonnet', async (modelId) => {
90
+ calls++
91
+ if (calls === 1) throw new Error('first fails')
92
+ return modelId
93
+ })
94
+ const result = await chain.execute()
95
+ expect(calls).toBe(2)
96
+ expect(result).toBeTruthy()
97
+ })
98
+ })
99
+
100
+ describe('tier helpers', () => {
101
+ it('tiersForModel returns expected tiers', () => {
102
+ expect(tiersForModel('sonnet')).toContain('immediate')
103
+ expect(tiersForModel('sonnet')).toContain('batch')
104
+ expect(tiersForModel('gpt-4o')).toContain('flex')
105
+ })
106
+
107
+ it('modelSupportsTier checks eligibility', () => {
108
+ expect(modelSupportsTier('gpt-4o', 'flex')).toBe(true)
109
+ expect(modelSupportsTier('sonnet', 'flex')).toBe(false)
110
+ expect(modelSupportsTier('sonnet', 'immediate')).toBe(true)
111
+ })
112
+
113
+ it('modelPolicyFor is re-exported', () => {
114
+ const p = modelPolicyFor('sonnet')
115
+ expect(p.$type).toBe('ModelPolicy')
116
+ })
117
+ })
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Verification for routing ALL dynamic code execution through ai-evaluate's
3
+ * V8-isolate sandbox (Cloudflare Dynamic Workers, Miniflare fallback in Node).
4
+ *
5
+ * Two distinct paths are exercised:
6
+ *
7
+ * - Path A — `type: 'code'` is DETERMINISTIC. A `handler` is a direct call; an
8
+ * inline `code` body runs in the sandbox. NO model is ever consulted. We spy
9
+ * on the model entry points and assert zero calls, and assert identical
10
+ * output across repeated calls.
11
+ *
12
+ * - Path B — `generateAndRunCode` is the NON-deterministic generate → run →
13
+ * test → return capability. The model AUTHORS the code; we mock that author
14
+ * step, but the run + test + return plumbing executes against the REAL
15
+ * Miniflare sandbox (no live Worker, no model).
16
+ *
17
+ * What is mocked: ONLY the model-author step in Path B (`generateObject` from
18
+ * `./generate.js`). The sandbox itself (Miniflare) is real. Path A mocks
19
+ * nothing — it only spies to prove the model is never touched.
20
+ */
21
+
22
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
23
+
24
+ // Spy targets for Path A: prove no model is consulted on the deterministic path.
25
+ import * as generateModule from '../src/generate.js'
26
+
27
+ describe('Path A — type:code is deterministic (no model, no network)', () => {
28
+ let generateObjectSpy: ReturnType<typeof vi.spyOn>
29
+ let generateTextSpy: ReturnType<typeof vi.spyOn>
30
+
31
+ beforeEach(() => {
32
+ generateObjectSpy = vi.spyOn(generateModule, 'generateObject')
33
+ generateTextSpy = vi.spyOn(generateModule, 'generateText')
34
+ })
35
+
36
+ afterEach(() => {
37
+ vi.restoreAllMocks()
38
+ })
39
+
40
+ it('handler functions are a direct call — deterministic, no model', async () => {
41
+ const { defineFunction } = await import('../src/function-registry.js')
42
+ const calculateTax = defineFunction<number, { amount: number; rate: number }>({
43
+ type: 'code',
44
+ name: 'calculateTax',
45
+ args: { amount: 'Amount (number)', rate: 'Rate (number)' },
46
+ handler: ({ amount, rate }) => amount * rate,
47
+ })
48
+
49
+ const a = await calculateTax.call({ amount: 100, rate: 0.2 })
50
+ const b = await calculateTax.call({ amount: 100, rate: 0.2 })
51
+
52
+ expect(a).toBe(20)
53
+ expect(b).toBe(20) // identical across repeated calls
54
+ expect(generateObjectSpy).not.toHaveBeenCalled()
55
+ expect(generateTextSpy).not.toHaveBeenCalled()
56
+ })
57
+
58
+ it('inline code bodies run in the sandbox — deterministic, no model', async () => {
59
+ const { defineFunction } = await import('../src/function-registry.js')
60
+ const sum = defineFunction<number, { items: number[] }>({
61
+ type: 'code',
62
+ name: 'sum',
63
+ args: { items: ['Numbers'] },
64
+ language: 'typescript',
65
+ code: 'return args.items.reduce((a, b) => a + b, 0)',
66
+ })
67
+
68
+ const a = await sum.call({ items: [1, 2, 3, 4] })
69
+ const b = await sum.call({ items: [1, 2, 3, 4] })
70
+
71
+ expect(a).toBe(10)
72
+ expect(b).toBe(10) // identical across repeated calls — fully deterministic
73
+ expect(generateObjectSpy).not.toHaveBeenCalled()
74
+ expect(generateTextSpy).not.toHaveBeenCalled()
75
+ })
76
+
77
+ it('an inline code body that throws surfaces the sandbox error', async () => {
78
+ const { defineFunction } = await import('../src/function-registry.js')
79
+ // Use an explicit statement body (contains `return`) so the runtime throw
80
+ // is reached rather than being mis-wrapped as a `return (expr)`.
81
+ const boom = defineFunction<number, Record<string, never>>({
82
+ type: 'code',
83
+ name: 'boom',
84
+ args: {},
85
+ language: 'typescript',
86
+ code: "if (true) { throw new Error('kaboom') }\nreturn 0",
87
+ })
88
+
89
+ await expect(boom.call({})).rejects.toThrow(/kaboom/)
90
+ expect(generateObjectSpy).not.toHaveBeenCalled()
91
+ })
92
+ }, 60000)
93
+
94
+ // Path B mocks ONLY the model-author step; the run+test runs in real Miniflare.
95
+ vi.mock('../src/generate.js', async (importOriginal) => {
96
+ const actual = await importOriginal<typeof import('../src/generate.js')>()
97
+ return { ...actual }
98
+ })
99
+
100
+ describe('Path B — generateAndRunCode: generate → run → test → return', () => {
101
+ beforeEach(() => {
102
+ vi.restoreAllMocks()
103
+ })
104
+
105
+ it('runs MODEL-AUTHORED code in the real sandbox and returns the computed result', async () => {
106
+ const gen = await import('../src/generate.js')
107
+
108
+ // Mock ONLY the model-author step. Everything downstream (run, test,
109
+ // return) executes against the REAL Miniflare sandbox.
110
+ const authoredModule = `export function calculateTax(args) {\n return args.amount * args.rate;\n}`
111
+ const authoredTests = `describe('calculateTax', () => {\n it('multiplies amount by rate', () => {\n expect(calculateTax({ amount: 100, rate: 0.2 })).toBe(20);\n });\n});`
112
+
113
+ const spy = vi.spyOn(gen, 'generateObject').mockResolvedValue({
114
+ object: { code: authoredModule, tests: authoredTests },
115
+ } as Awaited<ReturnType<typeof gen.generateObject>>)
116
+
117
+ const { generateAndRunCode } = await import('../src/function-registry.js')
118
+
119
+ const result = await generateAndRunCode<number, { amount: number; rate: number }>(
120
+ {
121
+ name: 'calculateTax',
122
+ description: 'Calculate tax owed',
123
+ args: { amount: '(number)', rate: '(number)' },
124
+ returnType: '(number)',
125
+ },
126
+ { amount: 100, rate: 0.2 }
127
+ )
128
+
129
+ // The model was consulted exactly once (the author step).
130
+ expect(spy).toHaveBeenCalledTimes(1)
131
+
132
+ // The RESULT was actually computed by running the authored code (not just
133
+ // returned as source).
134
+ expect(result.value).toBe(20)
135
+ expect(result.code).toContain('function calculateTax')
136
+
137
+ // Tests ran in the same sandbox and passed.
138
+ expect(result.testResults).toBeDefined()
139
+ expect(result.testResults!.failed).toBe(0)
140
+ expect(result.testResults!.passed).toBeGreaterThanOrEqual(1)
141
+ })
142
+
143
+ it('surfaces a sandbox failure when authored code throws at runtime', async () => {
144
+ const gen = await import('../src/generate.js')
145
+ vi.spyOn(gen, 'generateObject').mockResolvedValue({
146
+ object: { code: `export function bad(args) { throw new Error('runtime boom'); }` },
147
+ } as Awaited<ReturnType<typeof gen.generateObject>>)
148
+
149
+ const { generateAndRunCode } = await import('../src/function-registry.js')
150
+
151
+ await expect(
152
+ generateAndRunCode({ name: 'bad', args: { x: '(number)' }, includeTests: false }, { x: 1 })
153
+ ).rejects.toThrow(/runtime boom/)
154
+ })
155
+ }, 60000)
@@ -2,6 +2,7 @@
2
2
  * Tests for schema conversion
3
3
  *
4
4
  * These are pure unit tests - no AI calls needed.
5
+ * Uses type-safe instanceof checks instead of accessing internal _def property.
5
6
  */
6
7
 
7
8
  import { describe, it, expect } from 'vitest'
@@ -12,54 +13,67 @@ describe('schema', () => {
12
13
  describe('string types', () => {
13
14
  it('converts simple string description to z.string()', () => {
14
15
  const result = schema('User name')
15
- expect(result._def.typeName).toBe('ZodString')
16
- expect(result._def.description).toBe('User name')
16
+ expect(result instanceof z.ZodString).toBe(true)
17
+ expect(result.description).toBe('User name')
17
18
  })
18
19
 
19
20
  it('converts (number) hint to z.number()', () => {
20
21
  const result = schema('User age (number)')
21
- expect(result._def.typeName).toBe('ZodNumber')
22
- expect(result._def.description).toBe('User age')
22
+ expect(result instanceof z.ZodNumber).toBe(true)
23
+ expect(result.description).toBe('User age')
23
24
  })
24
25
 
25
26
  it('converts (boolean) hint to z.boolean()', () => {
26
27
  const result = schema('Is active (boolean)')
27
- expect(result._def.typeName).toBe('ZodBoolean')
28
- expect(result._def.description).toBe('Is active')
28
+ expect(result instanceof z.ZodBoolean).toBe(true)
29
+ expect(result.description).toBe('Is active')
29
30
  })
30
31
 
31
32
  it('converts (integer) hint to z.number().int()', () => {
32
33
  const result = schema('Item count (integer)')
33
- expect(result._def.typeName).toBe('ZodNumber')
34
- expect(result._def.checks?.some((c: { kind: string }) => c.kind === 'int')).toBe(true)
34
+ expect(result instanceof z.ZodNumber).toBe(true)
35
+ // Verify it's an integer by testing validation behavior
36
+ expect(result.safeParse(5).success).toBe(true)
37
+ expect(result.safeParse(5.5).success).toBe(false)
35
38
  })
36
39
 
37
40
  it('converts (date) hint to z.string().datetime()', () => {
38
41
  const result = schema('Created at (date)')
39
- expect(result._def.typeName).toBe('ZodString')
40
- expect(result._def.checks?.some((c: { kind: string }) => c.kind === 'datetime')).toBe(true)
42
+ expect(result instanceof z.ZodString).toBe(true)
43
+ // Verify it validates datetime format
44
+ expect(result.safeParse('2024-01-15T10:30:00Z').success).toBe(true)
45
+ expect(result.safeParse('not-a-date').success).toBe(false)
41
46
  })
42
47
  })
43
48
 
44
49
  describe('enum types', () => {
45
50
  it('converts pipe-separated values to z.enum()', () => {
46
51
  const result = schema('pending | done | cancelled')
47
- expect(result._def.typeName).toBe('ZodEnum')
48
- expect(result._def.values).toEqual(['pending', 'done', 'cancelled'])
52
+ expect(result instanceof z.ZodEnum).toBe(true)
53
+ // Verify enum values through validation
54
+ expect(result.safeParse('pending').success).toBe(true)
55
+ expect(result.safeParse('done').success).toBe(true)
56
+ expect(result.safeParse('cancelled').success).toBe(true)
57
+ expect(result.safeParse('invalid').success).toBe(false)
49
58
  })
50
59
 
51
60
  it('handles spaces around pipe', () => {
52
61
  const result = schema('yes | no | maybe')
53
- expect(result._def.values).toEqual(['yes', 'no', 'maybe'])
62
+ expect(result instanceof z.ZodEnum).toBe(true)
63
+ expect(result.safeParse('yes').success).toBe(true)
64
+ expect(result.safeParse('no').success).toBe(true)
65
+ expect(result.safeParse('maybe').success).toBe(true)
54
66
  })
55
67
  })
56
68
 
57
69
  describe('array types', () => {
58
70
  it('converts [string] to z.array(z.string())', () => {
59
71
  const result = schema(['List of items'])
60
- expect(result._def.typeName).toBe('ZodArray')
61
- expect(result._def.type._def.typeName).toBe('ZodString')
62
- expect(result._def.description).toBe('List of items')
72
+ expect(result instanceof z.ZodArray).toBe(true)
73
+ expect(result.description).toBe('List of items')
74
+ // Verify array of strings validation
75
+ expect(result.safeParse(['a', 'b', 'c']).success).toBe(true)
76
+ expect(result.safeParse([1, 2, 3]).success).toBe(false)
63
77
  })
64
78
  })
65
79
 
@@ -69,7 +83,10 @@ describe('schema', () => {
69
83
  name: 'User name',
70
84
  age: 'Age (number)',
71
85
  })
72
- expect(result._def.typeName).toBe('ZodObject')
86
+ expect(result instanceof z.ZodObject).toBe(true)
87
+ // Verify object validation
88
+ expect(result.safeParse({ name: 'John', age: 30 }).success).toBe(true)
89
+ expect(result.safeParse({ name: 'John', age: 'thirty' }).success).toBe(false)
73
90
  })
74
91
 
75
92
  it('handles nested objects', () => {
@@ -81,7 +98,16 @@ describe('schema', () => {
81
98
  },
82
99
  },
83
100
  })
84
- expect(result._def.typeName).toBe('ZodObject')
101
+ expect(result instanceof z.ZodObject).toBe(true)
102
+ // Verify nested object validation
103
+ expect(
104
+ result.safeParse({
105
+ user: {
106
+ name: 'John',
107
+ profile: { bio: 'A developer' },
108
+ },
109
+ }).success
110
+ ).toBe(true)
85
111
  })
86
112
 
87
113
  it('handles mixed types in object', () => {
@@ -92,7 +118,17 @@ describe('schema', () => {
92
118
  status: 'pending | done',
93
119
  tags: ['Tags'],
94
120
  })
95
- expect(result._def.typeName).toBe('ZodObject')
121
+ expect(result instanceof z.ZodObject).toBe(true)
122
+ // Verify mixed type validation
123
+ expect(
124
+ result.safeParse({
125
+ name: 'Test',
126
+ count: 5,
127
+ active: true,
128
+ status: 'pending',
129
+ tags: ['tag1', 'tag2'],
130
+ }).success
131
+ ).toBe(true)
96
132
  })
97
133
  })
98
134