digital-workers 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 +17 -0
- package/README.md +2 -0
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +33 -21
- package/dist/actions.js.map +1 -1
- package/dist/agent-comms.d.ts.map +1 -1
- package/dist/agent-comms.js +36 -25
- package/dist/agent-comms.js.map +1 -1
- package/dist/approve.d.ts +40 -8
- package/dist/approve.d.ts.map +1 -1
- package/dist/approve.js +86 -20
- package/dist/approve.js.map +1 -1
- package/dist/ask.d.ts +38 -7
- package/dist/ask.d.ts.map +1 -1
- package/dist/ask.js +85 -25
- package/dist/ask.js.map +1 -1
- package/dist/browse.d.ts +223 -0
- package/dist/browse.d.ts.map +1 -0
- package/dist/browse.js +392 -0
- package/dist/browse.js.map +1 -0
- package/dist/capability-tiers.js +3 -3
- package/dist/capability-tiers.js.map +1 -1
- package/dist/cascade-context.d.ts +28 -28
- package/dist/client.d.ts +162 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +64 -0
- package/dist/client.js.map +1 -0
- package/dist/decide.d.ts +42 -6
- package/dist/decide.d.ts.map +1 -1
- package/dist/decide.js +54 -11
- package/dist/decide.js.map +1 -1
- package/dist/do.d.ts +36 -7
- package/dist/do.d.ts.map +1 -1
- package/dist/do.js +82 -39
- package/dist/do.js.map +1 -1
- package/dist/error-escalation.d.ts.map +1 -1
- package/dist/error-escalation.js +38 -38
- package/dist/error-escalation.js.map +1 -1
- package/dist/generate.d.ts +48 -7
- package/dist/generate.d.ts.map +1 -1
- package/dist/generate.js +49 -8
- package/dist/generate.js.map +1 -1
- package/dist/goals.d.ts +10 -9
- package/dist/goals.d.ts.map +1 -1
- package/dist/goals.js +30 -24
- package/dist/goals.js.map +1 -1
- package/dist/image.d.ts +189 -0
- package/dist/image.d.ts.map +1 -0
- package/dist/image.js +528 -0
- package/dist/image.js.map +1 -0
- package/dist/index.d.ts +49 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +58 -2
- package/dist/index.js.map +1 -1
- package/dist/is.d.ts +45 -10
- package/dist/is.d.ts.map +1 -1
- package/dist/is.js +56 -21
- package/dist/is.js.map +1 -1
- package/dist/kpis.d.ts +24 -15
- package/dist/kpis.d.ts.map +1 -1
- package/dist/kpis.js +16 -14
- package/dist/kpis.js.map +1 -1
- package/dist/load-balancing.d.ts.map +1 -1
- package/dist/load-balancing.js +124 -38
- package/dist/load-balancing.js.map +1 -1
- package/dist/logger.d.ts +76 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +39 -0
- package/dist/logger.js.map +1 -0
- package/dist/notify.d.ts +38 -9
- package/dist/notify.d.ts.map +1 -1
- package/dist/notify.js +72 -17
- package/dist/notify.js.map +1 -1
- package/dist/role.d.ts +5 -4
- package/dist/role.d.ts.map +1 -1
- package/dist/role.js +13 -10
- package/dist/role.js.map +1 -1
- package/dist/runtime.d.ts +310 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +510 -0
- package/dist/runtime.js.map +1 -0
- package/dist/team.d.ts +11 -6
- package/dist/team.d.ts.map +1 -1
- package/dist/team.js +22 -15
- package/dist/team.js.map +1 -1
- package/dist/transports/email.d.ts +318 -0
- package/dist/transports/email.d.ts.map +1 -0
- package/dist/transports/email.js +779 -0
- package/dist/transports/email.js.map +1 -0
- package/dist/transports/slack.d.ts +515 -0
- package/dist/transports/slack.d.ts.map +1 -0
- package/dist/transports/slack.js +844 -0
- package/dist/transports/slack.js.map +1 -0
- package/dist/transports.d.ts.map +1 -1
- package/dist/transports.js +44 -25
- package/dist/transports.js.map +1 -1
- package/dist/types.d.ts +141 -19
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -1
- package/dist/utils/id.d.ts +19 -0
- package/dist/utils/id.d.ts.map +1 -0
- package/dist/utils/id.js +21 -0
- package/dist/utils/id.js.map +1 -0
- package/dist/video.d.ts +203 -0
- package/dist/video.d.ts.map +1 -0
- package/dist/video.js +528 -0
- package/dist/video.js.map +1 -0
- package/dist/worker.d.ts +343 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +698 -0
- package/dist/worker.js.map +1 -0
- package/package.json +32 -14
- package/src/actions.ts +39 -30
- package/src/agent-comms.ts +54 -92
- package/src/approve.ts +91 -20
- package/src/ask.ts +99 -25
- package/src/browse.ts +627 -0
- package/src/capability-tiers.ts +5 -5
- package/src/client.ts +221 -0
- package/src/decide.ts +81 -35
- package/src/do.ts +98 -52
- package/src/error-escalation.ts +55 -67
- package/src/generate.ts +52 -18
- package/src/goals.ts +36 -27
- package/src/image.ts +816 -0
- package/src/index.ts +187 -2
- package/src/is.ts +59 -25
- package/src/kpis.ts +41 -36
- package/src/load-balancing.ts +132 -46
- package/src/logger.ts +93 -0
- package/src/notify.ts +78 -17
- package/src/role.ts +30 -20
- package/src/runtime.ts +796 -0
- package/src/team.ts +24 -19
- package/src/transports/email.ts +1160 -0
- package/src/transports/slack.ts +1320 -0
- package/src/transports.ts +58 -43
- package/src/types.ts +174 -46
- package/src/utils/id.ts +21 -0
- package/src/video.ts +906 -0
- package/src/worker.ts +1007 -0
- package/test/approve.test.ts +305 -0
- package/test/ask.test.ts +274 -0
- package/test/browse.test.ts +361 -0
- package/test/decide.test.ts +252 -0
- package/test/do.test.ts +144 -0
- package/test/error-logging.test.ts +357 -0
- package/test/generate.test.ts +319 -0
- package/test/image.test.ts +398 -0
- package/test/is.test.ts +287 -0
- package/test/load-balancing-safety.test.ts +404 -0
- package/test/notify.test.ts +434 -0
- package/test/primitives.test.ts +320 -0
- package/test/runtime-integration.test.ts +892 -0
- package/test/transports/crypto.test.ts +230 -0
- package/test/transports/email.test.ts +866 -0
- package/test/transports/id-generation.test.ts +91 -0
- package/test/transports/slack.test.ts +760 -0
- package/test/type-safety.test.ts +834 -0
- package/test/types.test.ts +60 -2
- package/test/video.test.ts +530 -0
- package/test/worker.test.ts +1433 -0
- package/tsconfig.json +4 -1
- package/vitest.config.ts +42 -0
- package/wrangler.jsonc +36 -0
- package/LICENSE +0 -21
- package/src/actions.js +0 -436
- package/src/approve.js +0 -234
- package/src/ask.js +0 -226
- package/src/decide.js +0 -244
- package/src/do.js +0 -227
- package/src/generate.js +0 -298
- package/src/goals.js +0 -205
- package/src/index.js +0 -68
- package/src/is.js +0 -317
- package/src/kpis.js +0 -270
- package/src/notify.js +0 -219
- package/src/role.js +0 -110
- package/src/team.js +0 -130
- package/src/transports.js +0 -357
- package/src/types.js +0 -71
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for image() - Image generation primitive
|
|
3
|
+
*
|
|
4
|
+
* The image() function provides image generation with rich metadata
|
|
5
|
+
* about the generation process. It returns ImageResult with URL,
|
|
6
|
+
* prompt, and metadata (model, size, style, duration).
|
|
7
|
+
*
|
|
8
|
+
* These tests use real AI calls via the Cloudflare AI Gateway.
|
|
9
|
+
* Tests are skipped if AI_GATEWAY_URL and OPENAI_API_KEY are not configured.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect } from 'vitest'
|
|
13
|
+
import { image } from '../src/index.js'
|
|
14
|
+
|
|
15
|
+
// Skip tests if no gateway configured
|
|
16
|
+
const hasGateway = !!process.env.AI_GATEWAY_URL || !!process.env.IMAGE_GATEWAY_URL
|
|
17
|
+
const hasOpenAI = !!process.env.OPENAI_API_KEY
|
|
18
|
+
|
|
19
|
+
describe('image() - Image Generation Primitive', () => {
|
|
20
|
+
describe('Unit Tests (no AI)', () => {
|
|
21
|
+
it('should be exported from index', () => {
|
|
22
|
+
expect(image).toBeDefined()
|
|
23
|
+
expect(typeof image).toBe('function')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('should have variations method', () => {
|
|
27
|
+
expect(image.variations).toBeDefined()
|
|
28
|
+
expect(typeof image.variations).toBe('function')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should have edit method', () => {
|
|
32
|
+
expect(image.edit).toBeDefined()
|
|
33
|
+
expect(typeof image.edit).toBe('function')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('should have upscale method', () => {
|
|
37
|
+
expect(image.upscale).toBeDefined()
|
|
38
|
+
expect(typeof image.upscale).toBe('function')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('should have style method', () => {
|
|
42
|
+
expect(image.style).toBeDefined()
|
|
43
|
+
expect(typeof image.style).toBe('function')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('should have batch method', () => {
|
|
47
|
+
expect(image.batch).toBeDefined()
|
|
48
|
+
expect(typeof image.batch).toBe('function')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('should have aspectRatio method', () => {
|
|
52
|
+
expect(image.aspectRatio).toBeDefined()
|
|
53
|
+
expect(typeof image.aspectRatio).toBe('function')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('style method should return a function', () => {
|
|
57
|
+
const cinematicGenerator = image.style('cinematic')
|
|
58
|
+
expect(typeof cinematicGenerator).toBe('function')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('style method should work with all style presets', () => {
|
|
62
|
+
const styles = [
|
|
63
|
+
'realistic',
|
|
64
|
+
'artistic',
|
|
65
|
+
'cartoon',
|
|
66
|
+
'abstract',
|
|
67
|
+
'photographic',
|
|
68
|
+
'digital-art',
|
|
69
|
+
'cinematic',
|
|
70
|
+
] as const
|
|
71
|
+
|
|
72
|
+
styles.forEach((style) => {
|
|
73
|
+
const styledGenerator = image.style(style)
|
|
74
|
+
expect(typeof styledGenerator).toBe('function')
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
describe.skipIf(!hasGateway || !hasOpenAI)(
|
|
80
|
+
'Integration Tests (with AI) - Basic Generation',
|
|
81
|
+
() => {
|
|
82
|
+
it('should generate an image from a prompt', async () => {
|
|
83
|
+
const result = await image('A simple red circle on white background', {
|
|
84
|
+
size: '256x256',
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
expect(result).toBeDefined()
|
|
88
|
+
expect(result.url).toBeDefined()
|
|
89
|
+
expect(typeof result.url).toBe('string')
|
|
90
|
+
expect(result.url.length).toBeGreaterThan(0)
|
|
91
|
+
expect(result.prompt).toBe('A simple red circle on white background')
|
|
92
|
+
expect(result.metadata).toBeDefined()
|
|
93
|
+
expect(result.metadata.model).toBeDefined()
|
|
94
|
+
expect(result.metadata.size).toBe('256x256')
|
|
95
|
+
}, 60000) // 60 second timeout for image generation
|
|
96
|
+
|
|
97
|
+
it('should include duration in metadata', async () => {
|
|
98
|
+
const result = await image('A blue square', {
|
|
99
|
+
size: '256x256',
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
expect(result.metadata.duration).toBeDefined()
|
|
103
|
+
expect(typeof result.metadata.duration).toBe('number')
|
|
104
|
+
expect(result.metadata.duration).toBeGreaterThan(0)
|
|
105
|
+
}, 60000)
|
|
106
|
+
|
|
107
|
+
it('should support style option', async () => {
|
|
108
|
+
const result = await image('A mountain landscape', {
|
|
109
|
+
style: 'artistic',
|
|
110
|
+
size: '256x256',
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
expect(result).toBeDefined()
|
|
114
|
+
expect(result.url).toBeDefined()
|
|
115
|
+
expect(result.metadata.style).toBe('artistic')
|
|
116
|
+
}, 60000)
|
|
117
|
+
|
|
118
|
+
it('should support different sizes', async () => {
|
|
119
|
+
const sizes = ['256x256', '512x512', '1024x1024'] as const
|
|
120
|
+
|
|
121
|
+
for (const size of sizes) {
|
|
122
|
+
const result = await image('A green triangle', { size })
|
|
123
|
+
expect(result.metadata.size).toBe(size)
|
|
124
|
+
}
|
|
125
|
+
}, 180000) // 3 minutes for multiple generations
|
|
126
|
+
|
|
127
|
+
it('should return revised prompt when model modifies it', async () => {
|
|
128
|
+
const result = await image('A sunset', {
|
|
129
|
+
model: 'dall-e-3',
|
|
130
|
+
size: '1024x1024',
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
// DALL-E 3 typically revises prompts
|
|
134
|
+
expect(result.revisedPrompt).toBeDefined()
|
|
135
|
+
}, 60000)
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
describe.skipIf(!hasGateway || !hasOpenAI)('Integration Tests (with AI) - Style Presets', () => {
|
|
140
|
+
it('should generate realistic style image', async () => {
|
|
141
|
+
const result = await image('A coffee cup on a table', {
|
|
142
|
+
style: 'realistic',
|
|
143
|
+
size: '256x256',
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
expect(result.url).toBeDefined()
|
|
147
|
+
expect(result.metadata.style).toBe('realistic')
|
|
148
|
+
}, 60000)
|
|
149
|
+
|
|
150
|
+
it('should generate cartoon style image', async () => {
|
|
151
|
+
const result = await image('A happy dog', {
|
|
152
|
+
style: 'cartoon',
|
|
153
|
+
size: '256x256',
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
expect(result.url).toBeDefined()
|
|
157
|
+
expect(result.metadata.style).toBe('cartoon')
|
|
158
|
+
}, 60000)
|
|
159
|
+
|
|
160
|
+
it('should generate abstract style image', async () => {
|
|
161
|
+
const result = await image('Emotions and feelings', {
|
|
162
|
+
style: 'abstract',
|
|
163
|
+
size: '256x256',
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
expect(result.url).toBeDefined()
|
|
167
|
+
expect(result.metadata.style).toBe('abstract')
|
|
168
|
+
}, 60000)
|
|
169
|
+
|
|
170
|
+
it('should generate cinematic style image', async () => {
|
|
171
|
+
const result = await image('A hero standing on a cliff', {
|
|
172
|
+
style: 'cinematic',
|
|
173
|
+
size: '256x256',
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
expect(result.url).toBeDefined()
|
|
177
|
+
expect(result.metadata.style).toBe('cinematic')
|
|
178
|
+
}, 60000)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
describe.skipIf(!hasGateway || !hasOpenAI)(
|
|
182
|
+
'Integration Tests (with AI) - Styled Generator',
|
|
183
|
+
() => {
|
|
184
|
+
it('should create styled generator with image.style()', async () => {
|
|
185
|
+
const cartoonImage = image.style('cartoon')
|
|
186
|
+
|
|
187
|
+
const result = await cartoonImage('A friendly robot', {
|
|
188
|
+
size: '256x256',
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
expect(result).toBeDefined()
|
|
192
|
+
expect(result.url).toBeDefined()
|
|
193
|
+
expect(result.metadata.style).toBe('cartoon')
|
|
194
|
+
}, 60000)
|
|
195
|
+
|
|
196
|
+
it('should work with multiple styled generators', async () => {
|
|
197
|
+
const realisticGen = image.style('realistic')
|
|
198
|
+
const abstractGen = image.style('abstract')
|
|
199
|
+
|
|
200
|
+
const [realistic, abstract] = await Promise.all([
|
|
201
|
+
realisticGen('A flower', { size: '256x256' }),
|
|
202
|
+
abstractGen('A flower', { size: '256x256' }),
|
|
203
|
+
])
|
|
204
|
+
|
|
205
|
+
expect(realistic.metadata.style).toBe('realistic')
|
|
206
|
+
expect(abstract.metadata.style).toBe('abstract')
|
|
207
|
+
}, 120000)
|
|
208
|
+
}
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
describe.skipIf(!hasGateway || !hasOpenAI)(
|
|
212
|
+
'Integration Tests (with AI) - Batch Generation',
|
|
213
|
+
() => {
|
|
214
|
+
it('should generate multiple images in batch', async () => {
|
|
215
|
+
const prompts = ['A red apple', 'A blue berry', 'A yellow banana']
|
|
216
|
+
|
|
217
|
+
const results = await image.batch(prompts, {
|
|
218
|
+
size: '256x256',
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
expect(results).toBeDefined()
|
|
222
|
+
expect(Array.isArray(results)).toBe(true)
|
|
223
|
+
expect(results.length).toBe(3)
|
|
224
|
+
|
|
225
|
+
results.forEach((result, index) => {
|
|
226
|
+
expect(result.url).toBeDefined()
|
|
227
|
+
expect(result.prompt).toBe(prompts[index])
|
|
228
|
+
})
|
|
229
|
+
}, 180000)
|
|
230
|
+
|
|
231
|
+
it('should apply shared options to all images in batch', async () => {
|
|
232
|
+
const results = await image.batch(['A cat', 'A dog'], {
|
|
233
|
+
style: 'cartoon',
|
|
234
|
+
size: '256x256',
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
results.forEach((result) => {
|
|
238
|
+
expect(result.metadata.style).toBe('cartoon')
|
|
239
|
+
expect(result.metadata.size).toBe('256x256')
|
|
240
|
+
})
|
|
241
|
+
}, 120000)
|
|
242
|
+
}
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
describe.skipIf(!hasGateway || !hasOpenAI)('Integration Tests (with AI) - Aspect Ratio', () => {
|
|
246
|
+
it('should generate square image', async () => {
|
|
247
|
+
const result = await image.aspectRatio('A landscape scene', 'square')
|
|
248
|
+
|
|
249
|
+
expect(result.url).toBeDefined()
|
|
250
|
+
expect(result.metadata.size).toBe('1024x1024')
|
|
251
|
+
}, 60000)
|
|
252
|
+
|
|
253
|
+
it('should generate portrait image', async () => {
|
|
254
|
+
const result = await image.aspectRatio('A portrait of a person', 'portrait')
|
|
255
|
+
|
|
256
|
+
expect(result.url).toBeDefined()
|
|
257
|
+
expect(result.metadata.size).toBe('1024x1792')
|
|
258
|
+
}, 60000)
|
|
259
|
+
|
|
260
|
+
it('should generate landscape image', async () => {
|
|
261
|
+
const result = await image.aspectRatio('A wide mountain range', 'landscape')
|
|
262
|
+
|
|
263
|
+
expect(result.url).toBeDefined()
|
|
264
|
+
expect(result.metadata.size).toBe('1792x1024')
|
|
265
|
+
}, 60000)
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
describe.skipIf(!hasGateway || !hasOpenAI)('Integration Tests (with AI) - Variations', () => {
|
|
269
|
+
it('should generate variations of an image', async () => {
|
|
270
|
+
// First generate a source image
|
|
271
|
+
const source = await image('A simple geometric shape', {
|
|
272
|
+
size: '256x256',
|
|
273
|
+
model: 'dall-e-2', // Variations work with DALL-E 2
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
expect(source.url).toBeDefined()
|
|
277
|
+
|
|
278
|
+
// Then generate variations
|
|
279
|
+
const variations = await image.variations(source.url, {
|
|
280
|
+
count: 2,
|
|
281
|
+
size: '256x256',
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
expect(variations).toBeDefined()
|
|
285
|
+
expect(Array.isArray(variations)).toBe(true)
|
|
286
|
+
expect(variations.length).toBe(2)
|
|
287
|
+
|
|
288
|
+
variations.forEach((variation) => {
|
|
289
|
+
expect(variation.url).toBeDefined()
|
|
290
|
+
expect(variation.metadata.model).toBeDefined()
|
|
291
|
+
})
|
|
292
|
+
}, 180000)
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
describe.skipIf(!hasGateway || !hasOpenAI)('Integration Tests (with AI) - Editing', () => {
|
|
296
|
+
it('should edit an image with a prompt', async () => {
|
|
297
|
+
// First generate a source image
|
|
298
|
+
const source = await image('A room with a blank wall', {
|
|
299
|
+
size: '256x256',
|
|
300
|
+
model: 'dall-e-2', // Edits work with DALL-E 2
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
expect(source.url).toBeDefined()
|
|
304
|
+
|
|
305
|
+
// Edit the image
|
|
306
|
+
const edited = await image.edit(source.url, {
|
|
307
|
+
prompt: 'Add a colorful painting on the wall',
|
|
308
|
+
size: '256x256',
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
expect(edited).toBeDefined()
|
|
312
|
+
expect(edited.url).toBeDefined()
|
|
313
|
+
expect(edited.prompt).toBe('Add a colorful painting on the wall')
|
|
314
|
+
expect(edited.metadata.duration).toBeGreaterThan(0)
|
|
315
|
+
}, 180000)
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
describe.skipIf(!hasGateway)('Integration Tests (with AI) - Upscaling', () => {
|
|
319
|
+
it('should upscale an image', async () => {
|
|
320
|
+
// First generate a small source image
|
|
321
|
+
const source = await image('A detailed flower', {
|
|
322
|
+
size: '256x256',
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
expect(source.url).toBeDefined()
|
|
326
|
+
|
|
327
|
+
// Upscale it
|
|
328
|
+
const upscaled = await image.upscale(source.url, {
|
|
329
|
+
scale: 2,
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
expect(upscaled).toBeDefined()
|
|
333
|
+
expect(upscaled.url).toBeDefined()
|
|
334
|
+
expect(upscaled.scaleFactor).toBe(2)
|
|
335
|
+
expect(upscaled.upscaledSize.width).toBeGreaterThan(upscaled.originalSize.width)
|
|
336
|
+
expect(upscaled.upscaledSize.height).toBeGreaterThan(upscaled.originalSize.height)
|
|
337
|
+
}, 180000)
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
describe('Error Handling', () => {
|
|
341
|
+
it('should throw error for invalid image URL format in variations', async () => {
|
|
342
|
+
await expect(image.variations('not-a-valid-url', { count: 2 })).rejects.toThrow(
|
|
343
|
+
'Invalid image URL format'
|
|
344
|
+
)
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
it('should throw error for invalid image URL format in edit', async () => {
|
|
348
|
+
await expect(image.edit('not-a-valid-url', { prompt: 'test' })).rejects.toThrow(
|
|
349
|
+
'Invalid image URL format'
|
|
350
|
+
)
|
|
351
|
+
})
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
describe('Type Safety', () => {
|
|
355
|
+
it('should accept valid ImageOptions', () => {
|
|
356
|
+
const options: Parameters<typeof image>[1] = {
|
|
357
|
+
style: 'realistic',
|
|
358
|
+
size: '1024x1024',
|
|
359
|
+
model: 'dall-e-3',
|
|
360
|
+
quality: 'hd',
|
|
361
|
+
format: 'url',
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
expect(options.style).toBe('realistic')
|
|
365
|
+
expect(options.size).toBe('1024x1024')
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
it('should accept valid VariationOptions', () => {
|
|
369
|
+
const options: Parameters<typeof image.variations>[1] = {
|
|
370
|
+
count: 3,
|
|
371
|
+
size: '512x512',
|
|
372
|
+
format: 'url',
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
expect(options.count).toBe(3)
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
it('should accept valid EditOptions', () => {
|
|
379
|
+
const options: Parameters<typeof image.edit>[1] = {
|
|
380
|
+
prompt: 'Add a rainbow',
|
|
381
|
+
mask: 'https://example.com/mask.png',
|
|
382
|
+
size: '512x512',
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
expect(options.prompt).toBe('Add a rainbow')
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
it('should accept valid UpscaleOptions', () => {
|
|
389
|
+
const options: Parameters<typeof image.upscale>[1] = {
|
|
390
|
+
scale: 4,
|
|
391
|
+
model: 'real-esrgan',
|
|
392
|
+
denoise: 0.5,
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
expect(options.scale).toBe(4)
|
|
396
|
+
})
|
|
397
|
+
})
|
|
398
|
+
})
|
package/test/is.test.ts
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for is() - Type validation primitive
|
|
3
|
+
*
|
|
4
|
+
* The is() function provides comprehensive type/schema validation with detailed
|
|
5
|
+
* error messages and optional value coercion. Unlike ai-functions.is() which is
|
|
6
|
+
* a boolean assertion via LLM, this function returns TypeCheckResult with
|
|
7
|
+
* validation status, errors, and optionally coerced values.
|
|
8
|
+
*
|
|
9
|
+
* These tests use real AI calls via the Cloudflare AI Gateway.
|
|
10
|
+
* Tests are skipped if AI_GATEWAY_URL is not configured.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, it, expect } from 'vitest'
|
|
14
|
+
import { is } from '../src/index.js'
|
|
15
|
+
|
|
16
|
+
// Skip tests if no gateway configured
|
|
17
|
+
const hasGateway = !!process.env.AI_GATEWAY_URL || !!process.env.ANTHROPIC_API_KEY
|
|
18
|
+
|
|
19
|
+
describe('is() - Type Validation Primitive', () => {
|
|
20
|
+
describe('Unit Tests (no AI) - Built-in Types', () => {
|
|
21
|
+
it('should be exported from index', () => {
|
|
22
|
+
expect(is).toBeDefined()
|
|
23
|
+
expect(typeof is).toBe('function')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('should have email method', () => {
|
|
27
|
+
expect(is.email).toBeDefined()
|
|
28
|
+
expect(typeof is.email).toBe('function')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should have url method', () => {
|
|
32
|
+
expect(is.url).toBeDefined()
|
|
33
|
+
expect(typeof is.url).toBe('function')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('should have date method', () => {
|
|
37
|
+
expect(is.date).toBeDefined()
|
|
38
|
+
expect(typeof is.date).toBe('function')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('should have custom method', () => {
|
|
42
|
+
expect(is.custom).toBeDefined()
|
|
43
|
+
expect(typeof is.custom).toBe('function')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// Built-in type checks (no AI needed)
|
|
47
|
+
it('should validate string type', async () => {
|
|
48
|
+
const result = await is('hello', 'string')
|
|
49
|
+
expect(result.valid).toBe(true)
|
|
50
|
+
expect(result.value).toBe('hello')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('should validate number type', async () => {
|
|
54
|
+
const result = await is(42, 'number')
|
|
55
|
+
expect(result.valid).toBe(true)
|
|
56
|
+
expect(result.value).toBe(42)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('should invalidate wrong type', async () => {
|
|
60
|
+
const result = await is('hello', 'number')
|
|
61
|
+
expect(result.valid).toBe(false)
|
|
62
|
+
expect(result.errors).toBeDefined()
|
|
63
|
+
expect(Array.isArray(result.errors)).toBe(true)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('should validate boolean type', async () => {
|
|
67
|
+
const result = await is(true, 'boolean')
|
|
68
|
+
expect(result.valid).toBe(true)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('should validate array type', async () => {
|
|
72
|
+
const result = await is([1, 2, 3], 'array')
|
|
73
|
+
expect(result.valid).toBe(true)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('should validate object type', async () => {
|
|
77
|
+
const result = await is({ key: 'value' }, 'object')
|
|
78
|
+
expect(result.valid).toBe(true)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('should validate null type', async () => {
|
|
82
|
+
const result = await is(null, 'null')
|
|
83
|
+
expect(result.valid).toBe(true)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('should validate undefined type', async () => {
|
|
87
|
+
const result = await is(undefined, 'undefined')
|
|
88
|
+
expect(result.valid).toBe(true)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
// Coercion tests
|
|
92
|
+
it('should coerce string to number', async () => {
|
|
93
|
+
const result = await is('123', 'number', { coerce: true })
|
|
94
|
+
expect(result.valid).toBe(true)
|
|
95
|
+
expect(result.value).toBe(123)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('should coerce number to string', async () => {
|
|
99
|
+
const result = await is(42, 'string', { coerce: true })
|
|
100
|
+
expect(result.valid).toBe(true)
|
|
101
|
+
expect(result.value).toBe('42')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('should coerce "true" to boolean', async () => {
|
|
105
|
+
const result = await is('true', 'boolean', { coerce: true })
|
|
106
|
+
expect(result.valid).toBe(true)
|
|
107
|
+
expect(result.value).toBe(true)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('should coerce "false" to boolean', async () => {
|
|
111
|
+
const result = await is('false', 'boolean', { coerce: true })
|
|
112
|
+
expect(result.valid).toBe(true)
|
|
113
|
+
expect(result.value).toBe(false)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('should wrap value in array for array coercion', async () => {
|
|
117
|
+
const result = await is('single', 'array', { coerce: true })
|
|
118
|
+
expect(result.valid).toBe(true)
|
|
119
|
+
expect(result.value).toEqual(['single'])
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
describe('Unit Tests - Email Validation', () => {
|
|
124
|
+
it('should validate correct email', async () => {
|
|
125
|
+
const result = await is.email('user@example.com')
|
|
126
|
+
expect(result.valid).toBe(true)
|
|
127
|
+
expect(result.value).toBe('user@example.com')
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('should invalidate malformed email', async () => {
|
|
131
|
+
const result = await is.email('not-an-email')
|
|
132
|
+
expect(result.valid).toBe(false)
|
|
133
|
+
expect(result.errors).toContain('Invalid email format')
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('should invalidate email without domain', async () => {
|
|
137
|
+
const result = await is.email('user@')
|
|
138
|
+
expect(result.valid).toBe(false)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('should invalidate non-string for email', async () => {
|
|
142
|
+
const result = await is.email(123)
|
|
143
|
+
expect(result.valid).toBe(false)
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
describe('Unit Tests - URL Validation', () => {
|
|
148
|
+
it('should validate correct URL', async () => {
|
|
149
|
+
const result = await is.url('https://example.com')
|
|
150
|
+
expect(result.valid).toBe(true)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('should validate URL with path', async () => {
|
|
154
|
+
const result = await is.url('https://example.com/path/to/resource')
|
|
155
|
+
expect(result.valid).toBe(true)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('should invalidate malformed URL', async () => {
|
|
159
|
+
const result = await is.url('not-a-url')
|
|
160
|
+
expect(result.valid).toBe(false)
|
|
161
|
+
expect(result.errors).toContain('Invalid URL format')
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('should invalidate non-string for URL', async () => {
|
|
165
|
+
const result = await is.url(123)
|
|
166
|
+
expect(result.valid).toBe(false)
|
|
167
|
+
expect(result.errors).toContain('Value must be a string')
|
|
168
|
+
})
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
describe('Unit Tests - Date Validation', () => {
|
|
172
|
+
it('should validate Date object', async () => {
|
|
173
|
+
const date = new Date()
|
|
174
|
+
const result = await is.date(date)
|
|
175
|
+
expect(result.valid).toBe(true)
|
|
176
|
+
expect(result.value).toEqual(date)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('should invalidate invalid Date', async () => {
|
|
180
|
+
const result = await is.date(new Date('invalid'))
|
|
181
|
+
expect(result.valid).toBe(false)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('should coerce string to Date', async () => {
|
|
185
|
+
const result = await is.date('2024-01-15', { coerce: true })
|
|
186
|
+
expect(result.valid).toBe(true)
|
|
187
|
+
expect(result.value).toBeInstanceOf(Date)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('should coerce timestamp to Date', async () => {
|
|
191
|
+
const timestamp = Date.now()
|
|
192
|
+
const result = await is.date(timestamp, { coerce: true })
|
|
193
|
+
expect(result.valid).toBe(true)
|
|
194
|
+
expect(result.value).toBeInstanceOf(Date)
|
|
195
|
+
})
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
describe('Unit Tests - Custom Validation', () => {
|
|
199
|
+
it('should validate with custom sync function', async () => {
|
|
200
|
+
const result = await is.custom(42, (v) => typeof v === 'number' && (v as number) > 0)
|
|
201
|
+
expect(result.valid).toBe(true)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('should invalidate with custom sync function', async () => {
|
|
205
|
+
const result = await is.custom(-5, (v) => typeof v === 'number' && (v as number) > 0)
|
|
206
|
+
expect(result.valid).toBe(false)
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('should validate with custom async function', async () => {
|
|
210
|
+
const result = await is.custom('test', async (v) => typeof v === 'string')
|
|
211
|
+
expect(result.valid).toBe(true)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('should handle validation function errors', async () => {
|
|
215
|
+
const result = await is.custom('test', () => {
|
|
216
|
+
throw new Error('Custom error')
|
|
217
|
+
})
|
|
218
|
+
expect(result.valid).toBe(false)
|
|
219
|
+
expect(result.errors).toContain('Custom error')
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
describe.skipIf(!hasGateway)('Integration Tests (with AI) - Complex Types', () => {
|
|
224
|
+
it('should validate email type via AI', async () => {
|
|
225
|
+
const result = await is('test@example.com', 'email')
|
|
226
|
+
expect(result.valid).toBe(true)
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('should validate phone number type via AI', async () => {
|
|
230
|
+
const result = await is('+1-555-123-4567', 'phone number')
|
|
231
|
+
expect(result.valid).toBe(true)
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it('should validate US zip code via AI', async () => {
|
|
235
|
+
const result = await is('90210', 'US zip code')
|
|
236
|
+
expect(result.valid).toBe(true)
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it('should invalidate invalid zip code via AI', async () => {
|
|
240
|
+
const result = await is('ABCDE', 'US zip code')
|
|
241
|
+
expect(result.valid).toBe(false)
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it('should validate ISO date format via AI', async () => {
|
|
245
|
+
const result = await is('2024-01-15T10:30:00Z', 'ISO 8601 date')
|
|
246
|
+
expect(result.valid).toBe(true)
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
it('should validate against schema', async () => {
|
|
250
|
+
const result = await is(
|
|
251
|
+
{ name: 'John', age: 30 },
|
|
252
|
+
{
|
|
253
|
+
name: 'Person name (string)',
|
|
254
|
+
age: 'Age in years (number)',
|
|
255
|
+
}
|
|
256
|
+
)
|
|
257
|
+
expect(result.valid).toBe(true)
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it('should invalidate missing schema fields', async () => {
|
|
261
|
+
const result = await is(
|
|
262
|
+
{ name: 'John' },
|
|
263
|
+
{
|
|
264
|
+
name: 'Person name',
|
|
265
|
+
age: 'Age (required number)',
|
|
266
|
+
email: 'Email address (required)',
|
|
267
|
+
},
|
|
268
|
+
{ strict: true }
|
|
269
|
+
)
|
|
270
|
+
// In strict mode, missing required fields should fail
|
|
271
|
+
expect(result).toBeDefined()
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
it('should coerce values via AI when requested', async () => {
|
|
275
|
+
const result = await is(
|
|
276
|
+
{ age: '30', active: 'yes' },
|
|
277
|
+
{
|
|
278
|
+
age: 'Age in years (number)',
|
|
279
|
+
active: 'Is active (boolean)',
|
|
280
|
+
},
|
|
281
|
+
{ coerce: true }
|
|
282
|
+
)
|
|
283
|
+
expect(result).toBeDefined()
|
|
284
|
+
// The AI should attempt to coerce '30' to 30 and 'yes' to true
|
|
285
|
+
})
|
|
286
|
+
})
|
|
287
|
+
})
|