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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +90 -1
- package/README.md +38 -0
- package/dist/ai-promise.d.ts +3 -3
- package/dist/ai-promise.d.ts.map +1 -1
- package/dist/ai-promise.js +135 -64
- package/dist/ai-promise.js.map +1 -1
- package/dist/ai-schemas.d.ts +56 -0
- package/dist/ai-schemas.d.ts.map +1 -0
- package/dist/ai-schemas.js +53 -0
- package/dist/ai-schemas.js.map +1 -0
- package/dist/ai.d.ts +16 -242
- package/dist/ai.d.ts.map +1 -1
- package/dist/ai.js +51 -858
- package/dist/ai.js.map +1 -1
- package/dist/batch/anthropic.d.ts +6 -4
- package/dist/batch/anthropic.d.ts.map +1 -1
- package/dist/batch/anthropic.js +83 -145
- package/dist/batch/anthropic.js.map +1 -1
- package/dist/batch/bedrock.d.ts +8 -30
- package/dist/batch/bedrock.d.ts.map +1 -1
- package/dist/batch/bedrock.js +155 -338
- package/dist/batch/bedrock.js.map +1 -1
- package/dist/batch/cloudflare.d.ts +8 -20
- package/dist/batch/cloudflare.d.ts.map +1 -1
- package/dist/batch/cloudflare.js +68 -189
- package/dist/batch/cloudflare.js.map +1 -1
- package/dist/batch/google.d.ts +6 -20
- package/dist/batch/google.d.ts.map +1 -1
- package/dist/batch/google.js +70 -238
- package/dist/batch/google.js.map +1 -1
- package/dist/batch/index.d.ts +4 -1
- package/dist/batch/index.d.ts.map +1 -1
- package/dist/batch/index.js +4 -1
- package/dist/batch/index.js.map +1 -1
- package/dist/batch/memory.d.ts +1 -1
- package/dist/batch/memory.d.ts.map +1 -1
- package/dist/batch/memory.js +14 -10
- package/dist/batch/memory.js.map +1 -1
- package/dist/batch/openai.d.ts +11 -14
- package/dist/batch/openai.d.ts.map +1 -1
- package/dist/batch/openai.js +52 -156
- package/dist/batch/openai.js.map +1 -1
- package/dist/batch/provider.d.ts +111 -0
- package/dist/batch/provider.d.ts.map +1 -0
- package/dist/batch/provider.js +233 -0
- package/dist/batch/provider.js.map +1 -0
- package/dist/batch-map.d.ts.map +1 -1
- package/dist/batch-map.js +23 -17
- package/dist/batch-map.js.map +1 -1
- package/dist/batch-queue.d.ts +65 -0
- package/dist/batch-queue.d.ts.map +1 -1
- package/dist/batch-queue.js +169 -14
- package/dist/batch-queue.js.map +1 -1
- package/dist/budget.d.ts.map +1 -1
- package/dist/budget.js +27 -14
- package/dist/budget.js.map +1 -1
- package/dist/cache.d.ts +23 -0
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +36 -15
- package/dist/cache.js.map +1 -1
- package/dist/context.d.ts +26 -8
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +64 -62
- package/dist/context.js.map +1 -1
- package/dist/digital-objects-registry.d.ts +229 -0
- package/dist/digital-objects-registry.d.ts.map +1 -0
- package/dist/digital-objects-registry.js +617 -0
- package/dist/digital-objects-registry.js.map +1 -0
- package/dist/embeddings.d.ts +2 -2
- package/dist/embeddings.d.ts.map +1 -1
- package/dist/errors.d.ts +22 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +35 -0
- package/dist/errors.js.map +1 -0
- package/dist/eval/runner.d.ts +8 -0
- package/dist/eval/runner.d.ts.map +1 -1
- package/dist/eval/runner.js +41 -35
- package/dist/eval/runner.js.map +1 -1
- package/dist/eval-log/in-memory.d.ts +34 -0
- package/dist/eval-log/in-memory.d.ts.map +1 -0
- package/dist/eval-log/in-memory.js +84 -0
- package/dist/eval-log/in-memory.js.map +1 -0
- package/dist/eval-log/index.d.ts +29 -0
- package/dist/eval-log/index.d.ts.map +1 -0
- package/dist/eval-log/index.js +39 -0
- package/dist/eval-log/index.js.map +1 -0
- package/dist/eval-log/types.d.ts +101 -0
- package/dist/eval-log/types.d.ts.map +1 -0
- package/dist/eval-log/types.js +16 -0
- package/dist/eval-log/types.js.map +1 -0
- package/dist/function-registry.d.ts +176 -0
- package/dist/function-registry.d.ts.map +1 -0
- package/dist/function-registry.js +685 -0
- package/dist/function-registry.js.map +1 -0
- package/dist/generate.d.ts +9 -3
- package/dist/generate.d.ts.map +1 -1
- package/dist/generate.js +18 -18
- package/dist/generate.js.map +1 -1
- package/dist/index.d.ts +18 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +35 -18
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +118 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +187 -0
- package/dist/logger.js.map +1 -0
- package/dist/middleware/budget.d.ts +84 -0
- package/dist/middleware/budget.d.ts.map +1 -0
- package/dist/middleware/budget.js +110 -0
- package/dist/middleware/budget.js.map +1 -0
- package/dist/middleware/cache.d.ts +103 -0
- package/dist/middleware/cache.d.ts.map +1 -0
- package/dist/middleware/cache.js +228 -0
- package/dist/middleware/cache.js.map +1 -0
- package/dist/middleware/embed-cache.d.ts +99 -0
- package/dist/middleware/embed-cache.d.ts.map +1 -0
- package/dist/middleware/embed-cache.js +128 -0
- package/dist/middleware/embed-cache.js.map +1 -0
- package/dist/middleware/index.d.ts +11 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +11 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/trace.d.ts +103 -0
- package/dist/middleware/trace.d.ts.map +1 -0
- package/dist/middleware/trace.js +176 -0
- package/dist/middleware/trace.js.map +1 -0
- package/dist/primitives.d.ts +120 -1
- package/dist/primitives.d.ts.map +1 -1
- package/dist/primitives.js +398 -26
- package/dist/primitives.js.map +1 -1
- package/dist/retry.d.ts +66 -1
- package/dist/retry.d.ts.map +1 -1
- package/dist/retry.js +115 -8
- package/dist/retry.js.map +1 -1
- package/dist/sandbox.d.ts +36 -0
- package/dist/sandbox.d.ts.map +1 -0
- package/dist/sandbox.js +44 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/schema.js +2 -2
- package/dist/schema.js.map +1 -1
- package/dist/telemetry.d.ts +128 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +285 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/template.d.ts.map +1 -1
- package/dist/template.js +6 -1
- package/dist/template.js.map +1 -1
- package/dist/tool-orchestration.d.ts +66 -4
- package/dist/tool-orchestration.d.ts.map +1 -1
- package/dist/tool-orchestration.js +123 -23
- package/dist/tool-orchestration.js.map +1 -1
- package/dist/type-guards.d.ts +28 -0
- package/dist/type-guards.d.ts.map +1 -0
- package/dist/type-guards.js +29 -0
- package/dist/type-guards.js.map +1 -0
- package/dist/types.d.ts +155 -19
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +36 -1
- package/dist/types.js.map +1 -1
- package/dist/wrap-for-v3.d.ts +80 -0
- package/dist/wrap-for-v3.d.ts.map +1 -0
- package/dist/wrap-for-v3.js +89 -0
- package/dist/wrap-for-v3.js.map +1 -0
- package/examples/00-quickstart.ts +232 -0
- package/examples/01-rag-chatbot.ts +212 -0
- package/examples/02-multi-agent-research.ts +290 -0
- package/examples/03-email-classification.ts +379 -0
- package/examples/04-content-moderation.ts +400 -0
- package/examples/05-document-extraction.ts +455 -0
- package/examples/06-streaming-chat-nextjs.ts +437 -0
- package/examples/07-cloudflare-worker.ts +483 -0
- package/examples/08-batch-processing.ts +491 -0
- package/examples/09-budget-constrained.ts +527 -0
- package/examples/10-tool-orchestration.ts +565 -0
- package/examples/11-retry-resilience.ts +403 -0
- package/examples/12-caching-strategies.ts +422 -0
- package/examples/README.md +145 -0
- package/package.json +29 -25
- package/src/ai-promise.ts +226 -140
- package/src/ai-schemas.ts +122 -0
- package/src/ai.ts +71 -1176
- package/src/batch/anthropic.ts +96 -161
- package/src/batch/bedrock.ts +203 -454
- package/src/batch/cloudflare.ts +99 -282
- package/src/batch/google.ts +91 -297
- package/src/batch/index.ts +4 -1
- package/src/batch/memory.ts +15 -10
- package/src/batch/openai.ts +65 -193
- package/src/batch/provider.ts +336 -0
- package/src/batch-map.ts +29 -24
- package/src/batch-queue.ts +200 -11
- package/src/budget.ts +31 -18
- package/src/cache.ts +45 -17
- package/src/context.ts +106 -77
- package/src/digital-objects-registry.ts +750 -0
- package/src/errors.ts +37 -0
- package/src/eval/runner.ts +60 -36
- package/src/eval-log/in-memory.ts +90 -0
- package/src/eval-log/index.ts +46 -0
- package/src/eval-log/types.ts +110 -0
- package/src/function-registry.ts +874 -0
- package/src/generate.ts +33 -28
- package/src/index.ts +122 -21
- package/src/logger.ts +232 -0
- package/src/middleware/budget.ts +171 -0
- package/src/middleware/cache.ts +299 -0
- package/src/middleware/embed-cache.ts +195 -0
- package/src/middleware/index.ts +23 -0
- package/src/middleware/trace.ts +248 -0
- package/src/primitives.ts +589 -62
- package/src/retry.ts +144 -18
- package/src/sandbox.ts +52 -0
- package/src/schema.ts +8 -8
- package/src/telemetry.ts +403 -0
- package/src/template.ts +8 -4
- package/src/tool-orchestration.ts +213 -48
- package/src/type-guards.ts +31 -0
- package/src/types.ts +186 -27
- package/src/wrap-for-v3.ts +105 -0
- package/test/ai-promise.test.ts +1080 -0
- package/test/ai-proxy.test.ts +1 -1
- package/test/batch-autosubmit-errors.test.ts +49 -37
- package/test/batch-blog-posts.test.ts +87 -129
- package/test/core-functions.test.ts +183 -579
- package/test/decide.test.ts +154 -322
- package/test/define.test.ts +211 -8
- package/test/digital-objects-registry.test.ts +760 -0
- package/test/embedding-cache-middleware.test.ts +140 -0
- package/test/fill-template.test.ts +89 -0
- package/test/generate-core.test.ts +140 -229
- package/test/implicit-batch.test.ts +22 -65
- package/test/retry-policy-integration.test.ts +117 -0
- package/test/sandbox-execution.test.ts +155 -0
- package/test/schema.test.ts +55 -19
- package/test/template.test.ts +1164 -0
- package/test/tool-orchestration.test.ts +270 -0
- package/test/wrap-for-v3.test.ts +612 -0
- package/vitest.config.js +6 -0
- package/vitest.config.ts +20 -0
- package/LICENSE +0 -21
- package/dist/rpc/auth.d.ts +0 -69
- package/dist/rpc/auth.d.ts.map +0 -1
- package/dist/rpc/auth.js +0 -136
- package/dist/rpc/auth.js.map +0 -1
- package/dist/rpc/client.d.ts +0 -62
- package/dist/rpc/client.d.ts.map +0 -1
- package/dist/rpc/client.js +0 -103
- package/dist/rpc/client.js.map +0 -1
- package/dist/rpc/deferred.d.ts +0 -60
- package/dist/rpc/deferred.d.ts.map +0 -1
- package/dist/rpc/deferred.js +0 -96
- package/dist/rpc/deferred.js.map +0 -1
- package/dist/rpc/index.d.ts +0 -22
- package/dist/rpc/index.d.ts.map +0 -1
- package/dist/rpc/index.js +0 -38
- package/dist/rpc/index.js.map +0 -1
- package/dist/rpc/local.d.ts +0 -42
- package/dist/rpc/local.d.ts.map +0 -1
- package/dist/rpc/local.js +0 -50
- package/dist/rpc/local.js.map +0 -1
- package/dist/rpc/server.d.ts +0 -165
- package/dist/rpc/server.d.ts.map +0 -1
- package/dist/rpc/server.js +0 -405
- package/dist/rpc/server.js.map +0 -1
- package/dist/rpc/session.d.ts +0 -32
- package/dist/rpc/session.d.ts.map +0 -1
- package/dist/rpc/session.js +0 -43
- package/dist/rpc/session.js.map +0 -1
- package/dist/rpc/transport.d.ts +0 -306
- package/dist/rpc/transport.d.ts.map +0 -1
- package/dist/rpc/transport.js +0 -731
- package/dist/rpc/transport.js.map +0 -1
- package/src/batch/anthropic.js +0 -256
- package/src/batch/bedrock.js +0 -584
- package/src/batch/cloudflare.js +0 -287
- package/src/batch/google.js +0 -359
- package/src/batch/index.js +0 -30
- package/src/batch/memory.js +0 -187
- package/src/batch/openai.js +0 -402
- package/src/eval/index.js +0 -7
- package/src/eval/models.js +0 -119
- package/src/eval/runner.js +0 -147
- package/test/schema.test.js +0 -96
|
@@ -0,0 +1,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)
|
package/test/schema.test.ts
CHANGED
|
@@ -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.
|
|
16
|
-
expect(result.
|
|
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.
|
|
22
|
-
expect(result.
|
|
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.
|
|
28
|
-
expect(result.
|
|
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.
|
|
34
|
-
|
|
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.
|
|
40
|
-
|
|
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.
|
|
48
|
-
|
|
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.
|
|
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.
|
|
61
|
-
expect(result.
|
|
62
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|