digital-workers 2.1.1 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +136 -180
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +34 -21
- package/dist/actions.js.map +1 -1
- package/dist/agent-comms.d.ts +438 -0
- package/dist/agent-comms.d.ts.map +1 -0
- package/dist/agent-comms.js +677 -0
- package/dist/agent-comms.js.map +1 -0
- 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.d.ts +230 -0
- package/dist/capability-tiers.d.ts.map +1 -0
- package/dist/capability-tiers.js +388 -0
- package/dist/capability-tiers.js.map +1 -0
- package/dist/cascade-context.d.ts +523 -0
- package/dist/cascade-context.d.ts.map +1 -0
- package/dist/cascade-context.js +494 -0
- package/dist/cascade-context.js.map +1 -0
- 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 +416 -0
- package/dist/error-escalation.d.ts.map +1 -0
- package/dist/error-escalation.js +656 -0
- package/dist/error-escalation.js.map +1 -0
- 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 +59 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +92 -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 +395 -0
- package/dist/load-balancing.d.ts.map +1 -0
- package/dist/load-balancing.js +991 -0
- package/dist/load-balancing.js.map +1 -0
- 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 +149 -19
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +6 -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 +24 -5
- package/src/actions.ts +48 -38
- package/src/agent-comms.ts +1200 -0
- package/src/approve.ts +91 -20
- package/src/ask.ts +99 -25
- package/src/browse.ts +627 -0
- package/src/capability-tiers.ts +545 -0
- package/src/cascade-context.ts +648 -0
- package/src/client.ts +221 -0
- package/src/decide.ts +81 -35
- package/src/do.ts +98 -52
- package/src/error-escalation.ts +1123 -0
- package/src/generate.ts +52 -18
- package/src/goals.ts +36 -27
- package/src/image.ts +816 -0
- package/src/index.ts +410 -2
- package/src/is.ts +59 -25
- package/src/kpis.ts +41 -36
- package/src/load-balancing.ts +1467 -0
- 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 +182 -46
- package/src/utils/id.ts +21 -0
- package/src/video.ts +906 -0
- package/src/worker.ts +1007 -0
- package/test/agent-comms.test.ts +1397 -0
- package/test/approve.test.ts +305 -0
- package/test/ask.test.ts +274 -0
- package/test/browse.test.ts +361 -0
- package/test/capability-tiers.test.ts +631 -0
- package/test/cascade-context.test.ts +692 -0
- package/test/decide.test.ts +252 -0
- package/test/do.test.ts +144 -0
- package/test/error-escalation.test.ts +1205 -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/load-balancing-thread-safety.test.ts +464 -0
- package/test/load-balancing.test.ts +1145 -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 +95 -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/.turbo/turbo-build.log +0 -5
- 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
package/test/types.test.ts
CHANGED
|
@@ -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', () => {
|
|
@@ -76,6 +77,89 @@ describe('Worker Types', () => {
|
|
|
76
77
|
const statuses: WorkerStatus[] = ['available', 'busy', 'away', 'offline']
|
|
77
78
|
expect(statuses).toHaveLength(4)
|
|
78
79
|
})
|
|
80
|
+
|
|
81
|
+
it('should support capability tier', () => {
|
|
82
|
+
const agent: Worker = {
|
|
83
|
+
id: 'agent_1',
|
|
84
|
+
name: 'Agentic Worker',
|
|
85
|
+
type: 'agent',
|
|
86
|
+
status: 'available',
|
|
87
|
+
contacts: {
|
|
88
|
+
api: { endpoint: 'https://api.internal/agent', auth: 'bearer' },
|
|
89
|
+
},
|
|
90
|
+
capabilityTier: 'agentic',
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
expect(agent.capabilityTier).toBe('agentic')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('should support full capability profile', () => {
|
|
97
|
+
const agent: Worker = {
|
|
98
|
+
id: 'agent_2',
|
|
99
|
+
name: 'Code Worker',
|
|
100
|
+
type: 'agent',
|
|
101
|
+
status: 'available',
|
|
102
|
+
contacts: { api: 'https://api.internal/agent' },
|
|
103
|
+
capabilityTier: 'code',
|
|
104
|
+
capabilityProfile: {
|
|
105
|
+
name: 'basic-calculator',
|
|
106
|
+
tier: 'code',
|
|
107
|
+
complexityRating: 1,
|
|
108
|
+
tools: ['calculate', 'lookup'],
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
expect(agent.capabilityProfile?.tier).toBe('code')
|
|
113
|
+
expect(agent.capabilityProfile?.tools).toContain('calculate')
|
|
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
|
+
})
|
|
79
163
|
})
|
|
80
164
|
|
|
81
165
|
describe('WorkerRef interface', () => {
|
|
@@ -149,8 +233,17 @@ describe('Worker Types', () => {
|
|
|
149
233
|
|
|
150
234
|
it('should support all channel types', () => {
|
|
151
235
|
const channels: ContactChannel[] = [
|
|
152
|
-
'email',
|
|
153
|
-
'
|
|
236
|
+
'email',
|
|
237
|
+
'slack',
|
|
238
|
+
'teams',
|
|
239
|
+
'discord',
|
|
240
|
+
'phone',
|
|
241
|
+
'sms',
|
|
242
|
+
'whatsapp',
|
|
243
|
+
'telegram',
|
|
244
|
+
'web',
|
|
245
|
+
'api',
|
|
246
|
+
'webhook',
|
|
154
247
|
]
|
|
155
248
|
expect(channels).toHaveLength(11)
|
|
156
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
|
+
})
|