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.
Files changed (183) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +17 -0
  3. package/README.md +2 -0
  4. package/dist/actions.d.ts.map +1 -1
  5. package/dist/actions.js +33 -21
  6. package/dist/actions.js.map +1 -1
  7. package/dist/agent-comms.d.ts.map +1 -1
  8. package/dist/agent-comms.js +36 -25
  9. package/dist/agent-comms.js.map +1 -1
  10. package/dist/approve.d.ts +40 -8
  11. package/dist/approve.d.ts.map +1 -1
  12. package/dist/approve.js +86 -20
  13. package/dist/approve.js.map +1 -1
  14. package/dist/ask.d.ts +38 -7
  15. package/dist/ask.d.ts.map +1 -1
  16. package/dist/ask.js +85 -25
  17. package/dist/ask.js.map +1 -1
  18. package/dist/browse.d.ts +223 -0
  19. package/dist/browse.d.ts.map +1 -0
  20. package/dist/browse.js +392 -0
  21. package/dist/browse.js.map +1 -0
  22. package/dist/capability-tiers.js +3 -3
  23. package/dist/capability-tiers.js.map +1 -1
  24. package/dist/cascade-context.d.ts +28 -28
  25. package/dist/client.d.ts +162 -0
  26. package/dist/client.d.ts.map +1 -0
  27. package/dist/client.js +64 -0
  28. package/dist/client.js.map +1 -0
  29. package/dist/decide.d.ts +42 -6
  30. package/dist/decide.d.ts.map +1 -1
  31. package/dist/decide.js +54 -11
  32. package/dist/decide.js.map +1 -1
  33. package/dist/do.d.ts +36 -7
  34. package/dist/do.d.ts.map +1 -1
  35. package/dist/do.js +82 -39
  36. package/dist/do.js.map +1 -1
  37. package/dist/error-escalation.d.ts.map +1 -1
  38. package/dist/error-escalation.js +38 -38
  39. package/dist/error-escalation.js.map +1 -1
  40. package/dist/generate.d.ts +48 -7
  41. package/dist/generate.d.ts.map +1 -1
  42. package/dist/generate.js +49 -8
  43. package/dist/generate.js.map +1 -1
  44. package/dist/goals.d.ts +10 -9
  45. package/dist/goals.d.ts.map +1 -1
  46. package/dist/goals.js +30 -24
  47. package/dist/goals.js.map +1 -1
  48. package/dist/image.d.ts +189 -0
  49. package/dist/image.d.ts.map +1 -0
  50. package/dist/image.js +528 -0
  51. package/dist/image.js.map +1 -0
  52. package/dist/index.d.ts +49 -2
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +58 -2
  55. package/dist/index.js.map +1 -1
  56. package/dist/is.d.ts +45 -10
  57. package/dist/is.d.ts.map +1 -1
  58. package/dist/is.js +56 -21
  59. package/dist/is.js.map +1 -1
  60. package/dist/kpis.d.ts +24 -15
  61. package/dist/kpis.d.ts.map +1 -1
  62. package/dist/kpis.js +16 -14
  63. package/dist/kpis.js.map +1 -1
  64. package/dist/load-balancing.d.ts.map +1 -1
  65. package/dist/load-balancing.js +124 -38
  66. package/dist/load-balancing.js.map +1 -1
  67. package/dist/logger.d.ts +76 -0
  68. package/dist/logger.d.ts.map +1 -0
  69. package/dist/logger.js +39 -0
  70. package/dist/logger.js.map +1 -0
  71. package/dist/notify.d.ts +38 -9
  72. package/dist/notify.d.ts.map +1 -1
  73. package/dist/notify.js +72 -17
  74. package/dist/notify.js.map +1 -1
  75. package/dist/role.d.ts +5 -4
  76. package/dist/role.d.ts.map +1 -1
  77. package/dist/role.js +13 -10
  78. package/dist/role.js.map +1 -1
  79. package/dist/runtime.d.ts +310 -0
  80. package/dist/runtime.d.ts.map +1 -0
  81. package/dist/runtime.js +510 -0
  82. package/dist/runtime.js.map +1 -0
  83. package/dist/team.d.ts +11 -6
  84. package/dist/team.d.ts.map +1 -1
  85. package/dist/team.js +22 -15
  86. package/dist/team.js.map +1 -1
  87. package/dist/transports/email.d.ts +318 -0
  88. package/dist/transports/email.d.ts.map +1 -0
  89. package/dist/transports/email.js +779 -0
  90. package/dist/transports/email.js.map +1 -0
  91. package/dist/transports/slack.d.ts +515 -0
  92. package/dist/transports/slack.d.ts.map +1 -0
  93. package/dist/transports/slack.js +844 -0
  94. package/dist/transports/slack.js.map +1 -0
  95. package/dist/transports.d.ts.map +1 -1
  96. package/dist/transports.js +44 -25
  97. package/dist/transports.js.map +1 -1
  98. package/dist/types.d.ts +141 -19
  99. package/dist/types.d.ts.map +1 -1
  100. package/dist/types.js +5 -0
  101. package/dist/types.js.map +1 -1
  102. package/dist/utils/id.d.ts +19 -0
  103. package/dist/utils/id.d.ts.map +1 -0
  104. package/dist/utils/id.js +21 -0
  105. package/dist/utils/id.js.map +1 -0
  106. package/dist/video.d.ts +203 -0
  107. package/dist/video.d.ts.map +1 -0
  108. package/dist/video.js +528 -0
  109. package/dist/video.js.map +1 -0
  110. package/dist/worker.d.ts +343 -0
  111. package/dist/worker.d.ts.map +1 -0
  112. package/dist/worker.js +698 -0
  113. package/dist/worker.js.map +1 -0
  114. package/package.json +32 -14
  115. package/src/actions.ts +39 -30
  116. package/src/agent-comms.ts +54 -92
  117. package/src/approve.ts +91 -20
  118. package/src/ask.ts +99 -25
  119. package/src/browse.ts +627 -0
  120. package/src/capability-tiers.ts +5 -5
  121. package/src/client.ts +221 -0
  122. package/src/decide.ts +81 -35
  123. package/src/do.ts +98 -52
  124. package/src/error-escalation.ts +55 -67
  125. package/src/generate.ts +52 -18
  126. package/src/goals.ts +36 -27
  127. package/src/image.ts +816 -0
  128. package/src/index.ts +187 -2
  129. package/src/is.ts +59 -25
  130. package/src/kpis.ts +41 -36
  131. package/src/load-balancing.ts +132 -46
  132. package/src/logger.ts +93 -0
  133. package/src/notify.ts +78 -17
  134. package/src/role.ts +30 -20
  135. package/src/runtime.ts +796 -0
  136. package/src/team.ts +24 -19
  137. package/src/transports/email.ts +1160 -0
  138. package/src/transports/slack.ts +1320 -0
  139. package/src/transports.ts +58 -43
  140. package/src/types.ts +174 -46
  141. package/src/utils/id.ts +21 -0
  142. package/src/video.ts +906 -0
  143. package/src/worker.ts +1007 -0
  144. package/test/approve.test.ts +305 -0
  145. package/test/ask.test.ts +274 -0
  146. package/test/browse.test.ts +361 -0
  147. package/test/decide.test.ts +252 -0
  148. package/test/do.test.ts +144 -0
  149. package/test/error-logging.test.ts +357 -0
  150. package/test/generate.test.ts +319 -0
  151. package/test/image.test.ts +398 -0
  152. package/test/is.test.ts +287 -0
  153. package/test/load-balancing-safety.test.ts +404 -0
  154. package/test/notify.test.ts +434 -0
  155. package/test/primitives.test.ts +320 -0
  156. package/test/runtime-integration.test.ts +892 -0
  157. package/test/transports/crypto.test.ts +230 -0
  158. package/test/transports/email.test.ts +866 -0
  159. package/test/transports/id-generation.test.ts +91 -0
  160. package/test/transports/slack.test.ts +760 -0
  161. package/test/type-safety.test.ts +834 -0
  162. package/test/types.test.ts +60 -2
  163. package/test/video.test.ts +530 -0
  164. package/test/worker.test.ts +1433 -0
  165. package/tsconfig.json +4 -1
  166. package/vitest.config.ts +42 -0
  167. package/wrangler.jsonc +36 -0
  168. package/LICENSE +0 -21
  169. package/src/actions.js +0 -436
  170. package/src/approve.js +0 -234
  171. package/src/ask.js +0 -226
  172. package/src/decide.js +0 -244
  173. package/src/do.js +0 -227
  174. package/src/generate.js +0 -298
  175. package/src/goals.js +0 -205
  176. package/src/index.js +0 -68
  177. package/src/is.js +0 -317
  178. package/src/kpis.js +0 -270
  179. package/src/notify.js +0 -219
  180. package/src/role.js +0 -110
  181. package/src/team.js +0 -130
  182. package/src/transports.js +0 -357
  183. package/src/types.js +0 -71
@@ -24,6 +24,7 @@ import type {
24
24
  DoResult,
25
25
  ActionTarget,
26
26
  WorkerContext,
27
+ IdentityRef,
27
28
  } from '../src/types.js'
28
29
 
29
30
  describe('Worker Types', () => {
@@ -111,6 +112,54 @@ describe('Worker Types', () => {
111
112
  expect(agent.capabilityProfile?.tier).toBe('code')
112
113
  expect(agent.capabilityProfile?.tools).toContain('calculate')
113
114
  })
115
+
116
+ // aip-t86f: Worker.identity widened from string-only to schema.org.ai
117
+ // ThingRef (string | { $id, $type, name? }). Bare strings still work;
118
+ // typed refs let resolve() skip a fetch when $type is already known.
119
+ it('should support bare-string identity (back-compat with aip-ttfk)', () => {
120
+ const worker: Worker = {
121
+ id: 'worker_1',
122
+ name: 'Test Worker',
123
+ type: 'human',
124
+ status: 'available',
125
+ contacts: { email: 'alice@company.com' },
126
+ identity: 'identity:did:example:alice',
127
+ }
128
+
129
+ expect(worker.identity).toBe('identity:did:example:alice')
130
+ // Type-level: bare string is still a valid IdentityRef
131
+ const bare: IdentityRef = 'identity:did:example:alice'
132
+ expect(typeof bare).toBe('string')
133
+ })
134
+
135
+ it('should support typed ThingRef identity ({ $id, $type, name? })', () => {
136
+ const worker: Worker = {
137
+ id: 'worker_2',
138
+ name: 'Alice',
139
+ type: 'human',
140
+ status: 'available',
141
+ contacts: { email: 'alice@company.com' },
142
+ identity: {
143
+ $id: 'identity:did:example:alice',
144
+ $type: 'Identity',
145
+ name: 'Alice',
146
+ },
147
+ }
148
+
149
+ // Narrow on the typed-object branch
150
+ if (typeof worker.identity === 'object' && worker.identity !== null) {
151
+ expect(worker.identity.$id).toBe('identity:did:example:alice')
152
+ expect(worker.identity.$type).toBe('Identity')
153
+ expect(worker.identity.name).toBe('Alice')
154
+ } else {
155
+ // Force failure if the type narrowing didn't pick the object branch
156
+ expect.fail('expected typed ThingRef identity')
157
+ }
158
+
159
+ // Type-level: typed-ref shape is also a valid IdentityRef
160
+ const typed: IdentityRef = { $id: 'id:1', $type: 'Identity' }
161
+ expect(typed).toBeDefined()
162
+ })
114
163
  })
115
164
 
116
165
  describe('WorkerRef interface', () => {
@@ -184,8 +233,17 @@ describe('Worker Types', () => {
184
233
 
185
234
  it('should support all channel types', () => {
186
235
  const channels: ContactChannel[] = [
187
- 'email', 'slack', 'teams', 'discord', 'phone',
188
- 'sms', 'whatsapp', 'telegram', 'web', 'api', 'webhook',
236
+ 'email',
237
+ 'slack',
238
+ 'teams',
239
+ 'discord',
240
+ 'phone',
241
+ 'sms',
242
+ 'whatsapp',
243
+ 'telegram',
244
+ 'web',
245
+ 'api',
246
+ 'webhook',
189
247
  ]
190
248
  expect(channels).toHaveLength(11)
191
249
  })
@@ -0,0 +1,530 @@
1
+ /**
2
+ * Tests for video() - Video generation primitive
3
+ *
4
+ * The video() function provides AI-powered video generation with rich metadata
5
+ * about the generation process. It supports multiple models (Runway, Pika, etc.)
6
+ * and includes helper methods for image-to-video, extension, editing, and styling.
7
+ *
8
+ * These tests verify the structure and exports. Integration tests with actual
9
+ * video generation require provider API access and are skipped when unavailable.
10
+ */
11
+
12
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
13
+ // Import directly from video.ts for tests to work independently of build status
14
+ import { video } from '../src/video.js'
15
+ import type {
16
+ VideoOptions,
17
+ VideoResult,
18
+ VideoResolution,
19
+ VideoAspectRatio,
20
+ VideoModel,
21
+ VideoStyle,
22
+ VideoMetadata,
23
+ } from '../src/video.js'
24
+
25
+ // Mock fetch for unit tests
26
+ const mockFetch = vi.fn()
27
+ global.fetch = mockFetch
28
+
29
+ describe('video() - Video Generation Primitive', () => {
30
+ beforeEach(() => {
31
+ mockFetch.mockReset()
32
+ })
33
+
34
+ afterEach(() => {
35
+ vi.restoreAllMocks()
36
+ })
37
+
38
+ describe('Unit Tests - Exports and Structure', () => {
39
+ it('should be exported from index', () => {
40
+ expect(video).toBeDefined()
41
+ expect(typeof video).toBe('function')
42
+ })
43
+
44
+ it('should have fromImage method', () => {
45
+ expect(video.fromImage).toBeDefined()
46
+ expect(typeof video.fromImage).toBe('function')
47
+ })
48
+
49
+ it('should have extend method', () => {
50
+ expect(video.extend).toBeDefined()
51
+ expect(typeof video.extend).toBe('function')
52
+ })
53
+
54
+ it('should have edit method', () => {
55
+ expect(video.edit).toBeDefined()
56
+ expect(typeof video.edit).toBe('function')
57
+ })
58
+
59
+ it('should have style method', () => {
60
+ expect(video.style).toBeDefined()
61
+ expect(typeof video.style).toBe('function')
62
+ })
63
+
64
+ it('should have variations method', () => {
65
+ expect(video.variations).toBeDefined()
66
+ expect(typeof video.variations).toBe('function')
67
+ })
68
+
69
+ it('should have withMotion method', () => {
70
+ expect(video.withMotion).toBeDefined()
71
+ expect(typeof video.withMotion).toBe('function')
72
+ })
73
+
74
+ it('should have loop method', () => {
75
+ expect(video.loop).toBeDefined()
76
+ expect(typeof video.loop).toBe('function')
77
+ })
78
+ })
79
+
80
+ describe('Unit Tests - video() function', () => {
81
+ it('should call video worker with correct parameters', async () => {
82
+ mockFetch.mockResolvedValueOnce({
83
+ ok: true,
84
+ json: async () => ({
85
+ url: 'https://example.com/video.mp4',
86
+ thumbnail: 'https://example.com/thumb.jpg',
87
+ }),
88
+ })
89
+
90
+ const result = await video('A sunset over the ocean')
91
+
92
+ expect(mockFetch).toHaveBeenCalledTimes(1)
93
+ expect(mockFetch).toHaveBeenCalledWith(
94
+ expect.stringContaining('video.workers.do'),
95
+ expect.objectContaining({
96
+ method: 'POST',
97
+ headers: { 'Content-Type': 'application/json' },
98
+ })
99
+ )
100
+
101
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body)
102
+ expect(body.prompt).toBe('A sunset over the ocean')
103
+ expect(body.duration).toBe(4)
104
+ expect(body.fps).toBe(24)
105
+ expect(body.resolution).toBe('1080p')
106
+ })
107
+
108
+ it('should return VideoResult with correct structure', async () => {
109
+ mockFetch.mockResolvedValueOnce({
110
+ ok: true,
111
+ json: async () => ({
112
+ url: 'https://example.com/video.mp4',
113
+ thumbnail: 'https://example.com/thumb.jpg',
114
+ fileSize: 1024000,
115
+ format: 'mp4',
116
+ seed: 12345,
117
+ }),
118
+ })
119
+
120
+ const result = await video('Test prompt')
121
+
122
+ expect(result).toHaveProperty('url')
123
+ expect(result).toHaveProperty('prompt')
124
+ expect(result).toHaveProperty('metadata')
125
+ expect(result).toHaveProperty('status')
126
+
127
+ expect(result.url).toBe('https://example.com/video.mp4')
128
+ expect(result.prompt).toBe('Test prompt')
129
+ expect(result.status).toBe('completed')
130
+
131
+ expect(result.metadata).toHaveProperty('model')
132
+ expect(result.metadata).toHaveProperty('duration')
133
+ expect(result.metadata).toHaveProperty('resolution')
134
+ expect(result.metadata).toHaveProperty('fps')
135
+ expect(result.metadata).toHaveProperty('aspectRatio')
136
+ expect(result.metadata).toHaveProperty('generationTime')
137
+ })
138
+
139
+ it('should accept all VideoOptions', async () => {
140
+ mockFetch.mockResolvedValueOnce({
141
+ ok: true,
142
+ json: async () => ({ url: 'https://example.com/video.mp4' }),
143
+ })
144
+
145
+ await video('A cinematic scene', {
146
+ duration: 8,
147
+ fps: 30,
148
+ resolution: '4k',
149
+ aspectRatio: '21:9',
150
+ style: 'cinematic',
151
+ model: 'runway-gen3',
152
+ negativePrompt: 'blurry, distorted',
153
+ guidance: 12,
154
+ seed: 42,
155
+ motion: 'dolly',
156
+ motionIntensity: 0.7,
157
+ loop: true,
158
+ })
159
+
160
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body)
161
+ expect(body.duration).toBe(8)
162
+ expect(body.fps).toBe(30)
163
+ expect(body.resolution).toBe('4k')
164
+ expect(body.aspectRatio).toBe('21:9')
165
+ expect(body.style).toBe('cinematic')
166
+ expect(body.model).toBe('runway-gen3')
167
+ expect(body.negativePrompt).toBe('blurry, distorted')
168
+ expect(body.guidance).toBe(12)
169
+ expect(body.seed).toBe(42)
170
+ expect(body.motion).toBe('dolly')
171
+ expect(body.motionIntensity).toBe(0.7)
172
+ expect(body.loop).toBe(true)
173
+ })
174
+
175
+ it('should handle API errors gracefully', async () => {
176
+ mockFetch.mockResolvedValueOnce({
177
+ ok: false,
178
+ status: 500,
179
+ text: async () => 'Internal server error',
180
+ })
181
+
182
+ const result = await video('Test prompt')
183
+
184
+ expect(result.status).toBe('failed')
185
+ expect(result.error).toContain('500')
186
+ expect(result.url).toBe('')
187
+ })
188
+
189
+ it('should handle network errors gracefully', async () => {
190
+ mockFetch.mockRejectedValueOnce(new Error('Network error'))
191
+
192
+ const result = await video('Test prompt')
193
+
194
+ expect(result.status).toBe('failed')
195
+ expect(result.error).toBe('Network error')
196
+ expect(result.url).toBe('')
197
+ })
198
+ })
199
+
200
+ describe('Unit Tests - video.fromImage()', () => {
201
+ it('should send image-to-video request', async () => {
202
+ mockFetch.mockResolvedValueOnce({
203
+ ok: true,
204
+ json: async () => ({ url: 'https://example.com/animated.mp4' }),
205
+ })
206
+
207
+ const result = await video.fromImage(
208
+ 'https://example.com/image.jpg',
209
+ 'Animate the clouds moving'
210
+ )
211
+
212
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body)
213
+ expect(body.mode).toBe('image-to-video')
214
+ expect(body.imageUrl).toBe('https://example.com/image.jpg')
215
+ expect(body.prompt).toBe('Animate the clouds moving')
216
+ })
217
+
218
+ it('should support imageFidelity option', async () => {
219
+ mockFetch.mockResolvedValueOnce({
220
+ ok: true,
221
+ json: async () => ({ url: 'https://example.com/animated.mp4' }),
222
+ })
223
+
224
+ await video.fromImage('https://example.com/image.jpg', 'Animate', { imageFidelity: 0.9 })
225
+
226
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body)
227
+ expect(body.imageFidelity).toBe(0.9)
228
+ })
229
+ })
230
+
231
+ describe('Unit Tests - video.extend()', () => {
232
+ it('should send extension request', async () => {
233
+ mockFetch.mockResolvedValueOnce({
234
+ ok: true,
235
+ json: async () => ({
236
+ url: 'https://example.com/extended.mp4',
237
+ totalDuration: 10,
238
+ }),
239
+ })
240
+
241
+ const result = await video.extend('https://example.com/original.mp4', 4)
242
+
243
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body)
244
+ expect(body.mode).toBe('extend')
245
+ expect(body.videoUrl).toBe('https://example.com/original.mp4')
246
+ expect(body.duration).toBe(4)
247
+ })
248
+
249
+ it('should support direction option', async () => {
250
+ mockFetch.mockResolvedValueOnce({
251
+ ok: true,
252
+ json: async () => ({ url: 'https://example.com/extended.mp4' }),
253
+ })
254
+
255
+ await video.extend('https://example.com/original.mp4', 4, {
256
+ direction: 'backward',
257
+ prompt: 'Show what happened before',
258
+ })
259
+
260
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body)
261
+ expect(body.direction).toBe('backward')
262
+ expect(body.prompt).toBe('Show what happened before')
263
+ })
264
+ })
265
+
266
+ describe('Unit Tests - video.edit()', () => {
267
+ it('should send edit request', async () => {
268
+ mockFetch.mockResolvedValueOnce({
269
+ ok: true,
270
+ json: async () => ({ url: 'https://example.com/edited.mp4' }),
271
+ })
272
+
273
+ const result = await video.edit(
274
+ 'https://example.com/original.mp4',
275
+ 'Make it look like vintage film'
276
+ )
277
+
278
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body)
279
+ expect(body.mode).toBe('edit')
280
+ expect(body.videoUrl).toBe('https://example.com/original.mp4')
281
+ expect(body.prompt).toBe('Make it look like vintage film')
282
+ })
283
+
284
+ it('should support mask and region options', async () => {
285
+ mockFetch.mockResolvedValueOnce({
286
+ ok: true,
287
+ json: async () => ({ url: 'https://example.com/edited.mp4' }),
288
+ })
289
+
290
+ await video.edit('https://example.com/original.mp4', 'Replace the background', {
291
+ maskUrl: 'https://example.com/mask.png',
292
+ region: { x: 0, y: 0, width: 100, height: 100 },
293
+ strength: 0.9,
294
+ preserveAudio: false,
295
+ })
296
+
297
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body)
298
+ expect(body.maskUrl).toBe('https://example.com/mask.png')
299
+ expect(body.region).toEqual({ x: 0, y: 0, width: 100, height: 100 })
300
+ expect(body.strength).toBe(0.9)
301
+ expect(body.preserveAudio).toBe(false)
302
+ })
303
+ })
304
+
305
+ describe('Unit Tests - video.style()', () => {
306
+ it('should return a styled video generator', () => {
307
+ const cinematicVideo = video.style('cinematic')
308
+ expect(typeof cinematicVideo).toBe('function')
309
+ })
310
+
311
+ it('should apply style to generated videos', async () => {
312
+ mockFetch.mockResolvedValueOnce({
313
+ ok: true,
314
+ json: async () => ({ url: 'https://example.com/video.mp4' }),
315
+ })
316
+
317
+ const animeVideo = video.style('anime')
318
+ await animeVideo('A magical transformation sequence')
319
+
320
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body)
321
+ expect(body.style).toBe('anime')
322
+ })
323
+
324
+ it('should allow additional options', async () => {
325
+ mockFetch.mockResolvedValueOnce({
326
+ ok: true,
327
+ json: async () => ({ url: 'https://example.com/video.mp4' }),
328
+ })
329
+
330
+ const noirVideo = video.style('noir')
331
+ await noirVideo('A detective in a dark alley', { duration: 10, resolution: '4k' })
332
+
333
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body)
334
+ expect(body.style).toBe('noir')
335
+ expect(body.duration).toBe(10)
336
+ expect(body.resolution).toBe('4k')
337
+ })
338
+ })
339
+
340
+ describe('Unit Tests - video.variations()', () => {
341
+ it('should generate multiple variations', async () => {
342
+ mockFetch
343
+ .mockResolvedValueOnce({
344
+ ok: true,
345
+ json: async () => ({ url: 'https://example.com/video1.mp4' }),
346
+ })
347
+ .mockResolvedValueOnce({
348
+ ok: true,
349
+ json: async () => ({ url: 'https://example.com/video2.mp4' }),
350
+ })
351
+ .mockResolvedValueOnce({
352
+ ok: true,
353
+ json: async () => ({ url: 'https://example.com/video3.mp4' }),
354
+ })
355
+
356
+ const results = await video.variations('A cat playing', 3)
357
+
358
+ expect(results).toHaveLength(3)
359
+ expect(mockFetch).toHaveBeenCalledTimes(3)
360
+
361
+ // Each call should have a different seed
362
+ const seeds = mockFetch.mock.calls.map((call) => JSON.parse(call[1].body).seed)
363
+ expect(new Set(seeds).size).toBe(3) // All unique seeds
364
+ })
365
+
366
+ it('should respect provided seed and increment', async () => {
367
+ mockFetch.mockResolvedValue({
368
+ ok: true,
369
+ json: async () => ({ url: 'https://example.com/video.mp4' }),
370
+ })
371
+
372
+ await video.variations('Test', 3, { seed: 100 })
373
+
374
+ const seeds = mockFetch.mock.calls.map((call) => JSON.parse(call[1].body).seed)
375
+ expect(seeds).toEqual([100, 101, 102])
376
+ })
377
+ })
378
+
379
+ describe('Unit Tests - video.withMotion()', () => {
380
+ it('should apply motion type', async () => {
381
+ mockFetch.mockResolvedValueOnce({
382
+ ok: true,
383
+ json: async () => ({ url: 'https://example.com/video.mp4' }),
384
+ })
385
+
386
+ await video.withMotion('A beautiful landscape', 'pan')
387
+
388
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body)
389
+ expect(body.motion).toBe('pan')
390
+ })
391
+
392
+ it('should accept motion intensity option', async () => {
393
+ mockFetch.mockResolvedValueOnce({
394
+ ok: true,
395
+ json: async () => ({ url: 'https://example.com/video.mp4' }),
396
+ })
397
+
398
+ await video.withMotion('A city skyline', 'orbit', { motionIntensity: 0.3 })
399
+
400
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body)
401
+ expect(body.motion).toBe('orbit')
402
+ expect(body.motionIntensity).toBe(0.3)
403
+ })
404
+ })
405
+
406
+ describe('Unit Tests - video.loop()', () => {
407
+ it('should set loop to true', async () => {
408
+ mockFetch.mockResolvedValueOnce({
409
+ ok: true,
410
+ json: async () => ({ url: 'https://example.com/video.mp4' }),
411
+ })
412
+
413
+ await video.loop('Swaying grass')
414
+
415
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body)
416
+ expect(body.loop).toBe(true)
417
+ })
418
+ })
419
+
420
+ describe('Type Safety Tests', () => {
421
+ it('should accept valid resolution types', async () => {
422
+ mockFetch.mockResolvedValue({
423
+ ok: true,
424
+ json: async () => ({ url: 'https://example.com/video.mp4' }),
425
+ })
426
+
427
+ const resolutions: VideoResolution[] = ['480p', '720p', '1080p', '4k']
428
+ for (const resolution of resolutions) {
429
+ await video('Test', { resolution })
430
+ }
431
+ expect(mockFetch).toHaveBeenCalledTimes(4)
432
+ })
433
+
434
+ it('should accept valid aspect ratios', async () => {
435
+ mockFetch.mockResolvedValue({
436
+ ok: true,
437
+ json: async () => ({ url: 'https://example.com/video.mp4' }),
438
+ })
439
+
440
+ const ratios: VideoAspectRatio[] = ['16:9', '9:16', '1:1', '4:3', '21:9']
441
+ for (const aspectRatio of ratios) {
442
+ await video('Test', { aspectRatio })
443
+ }
444
+ expect(mockFetch).toHaveBeenCalledTimes(5)
445
+ })
446
+
447
+ it('should accept valid model types', async () => {
448
+ mockFetch.mockResolvedValue({
449
+ ok: true,
450
+ json: async () => ({ url: 'https://example.com/video.mp4' }),
451
+ })
452
+
453
+ const models: VideoModel[] = [
454
+ 'runway-gen3',
455
+ 'runway-gen2',
456
+ 'pika-1.0',
457
+ 'pika-1.5',
458
+ 'stable-video',
459
+ 'minimax',
460
+ 'kling',
461
+ 'luma',
462
+ ]
463
+ for (const model of models) {
464
+ await video('Test', { model })
465
+ }
466
+ expect(mockFetch).toHaveBeenCalledTimes(8)
467
+ })
468
+
469
+ it('should accept valid style presets', async () => {
470
+ mockFetch.mockResolvedValue({
471
+ ok: true,
472
+ json: async () => ({ url: 'https://example.com/video.mp4' }),
473
+ })
474
+
475
+ const styles: VideoStyle[] = [
476
+ 'cinematic',
477
+ 'anime',
478
+ 'realistic',
479
+ 'cartoon',
480
+ 'documentary',
481
+ 'vintage',
482
+ 'noir',
483
+ 'fantasy',
484
+ 'sci-fi',
485
+ ]
486
+ for (const style of styles) {
487
+ await video('Test', { style })
488
+ }
489
+ expect(mockFetch).toHaveBeenCalledTimes(9)
490
+ })
491
+ })
492
+
493
+ describe('Metadata Tests', () => {
494
+ it('should include all expected metadata fields', async () => {
495
+ mockFetch.mockResolvedValueOnce({
496
+ ok: true,
497
+ json: async () => ({
498
+ url: 'https://example.com/video.mp4',
499
+ fileSize: 2048000,
500
+ format: 'webm',
501
+ seed: 54321,
502
+ cost: 0.05,
503
+ }),
504
+ })
505
+
506
+ const result = await video('Test prompt', {
507
+ style: 'cinematic',
508
+ duration: 6,
509
+ fps: 30,
510
+ resolution: '4k',
511
+ aspectRatio: '21:9',
512
+ model: 'runway-gen3',
513
+ })
514
+
515
+ const { metadata } = result
516
+ expect(metadata.model).toBe('runway-gen3')
517
+ expect(metadata.duration).toBe(6)
518
+ expect(metadata.resolution).toBe('4k')
519
+ expect(metadata.fps).toBe(30)
520
+ expect(metadata.aspectRatio).toBe('21:9')
521
+ expect(metadata.style).toBe('cinematic')
522
+ expect(metadata.fileSize).toBe(2048000)
523
+ expect(metadata.format).toBe('webm')
524
+ expect(metadata.seed).toBe(54321)
525
+ expect(metadata.cost).toBe(0.05)
526
+ expect(typeof metadata.generationTime).toBe('number')
527
+ expect(metadata.generationTime).toBeGreaterThanOrEqual(0)
528
+ })
529
+ })
530
+ })