digital-workers 2.1.3 → 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 +9 -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/.turbo/turbo-build.log +0 -4
- 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,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for approve() - Approval workflow primitive
|
|
3
|
+
*
|
|
4
|
+
* The approve() function routes approval requests to Workers (Humans or AI Agents)
|
|
5
|
+
* via real communication channels and waits for actual approval. Unlike
|
|
6
|
+
* ai-functions.approve() which generates approval content, this function
|
|
7
|
+
* implements real approval workflows.
|
|
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 { approve } from '../src/index.js'
|
|
15
|
+
import type { Worker, WorkerRef, WorkerTeam } from '../src/types.js'
|
|
16
|
+
|
|
17
|
+
// Skip tests if no gateway configured
|
|
18
|
+
const hasGateway = !!process.env.AI_GATEWAY_URL || !!process.env.ANTHROPIC_API_KEY
|
|
19
|
+
|
|
20
|
+
// Test fixtures
|
|
21
|
+
const manager: Worker = {
|
|
22
|
+
id: 'manager',
|
|
23
|
+
name: 'Manager',
|
|
24
|
+
type: 'human',
|
|
25
|
+
status: 'available',
|
|
26
|
+
contacts: {
|
|
27
|
+
email: 'manager@example.com',
|
|
28
|
+
slack: '@manager',
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const cto: WorkerRef = {
|
|
33
|
+
id: 'cto',
|
|
34
|
+
name: 'CTO',
|
|
35
|
+
type: 'human',
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const finance: Worker = {
|
|
39
|
+
id: 'finance',
|
|
40
|
+
name: 'Finance',
|
|
41
|
+
type: 'human',
|
|
42
|
+
status: 'available',
|
|
43
|
+
contacts: {
|
|
44
|
+
email: 'finance@example.com',
|
|
45
|
+
slack: '#finance',
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const approvalTeam: WorkerTeam = {
|
|
50
|
+
id: 'approval-team',
|
|
51
|
+
name: 'Approval Team',
|
|
52
|
+
members: [manager, finance],
|
|
53
|
+
contacts: {
|
|
54
|
+
slack: '#approvals',
|
|
55
|
+
email: 'approvals@example.com',
|
|
56
|
+
},
|
|
57
|
+
lead: manager,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe('approve() - Approval Workflow Primitive', () => {
|
|
61
|
+
describe('Unit Tests (no AI)', () => {
|
|
62
|
+
it('should be exported from index', () => {
|
|
63
|
+
expect(approve).toBeDefined()
|
|
64
|
+
expect(typeof approve).toBe('function')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('should have withContext method', () => {
|
|
68
|
+
expect(approve.withContext).toBeDefined()
|
|
69
|
+
expect(typeof approve.withContext).toBe('function')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('should have batch method', () => {
|
|
73
|
+
expect(approve.batch).toBeDefined()
|
|
74
|
+
expect(typeof approve.batch).toBe('function')
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('should have withDeadline method', () => {
|
|
78
|
+
expect(approve.withDeadline).toBeDefined()
|
|
79
|
+
expect(typeof approve.withDeadline).toBe('function')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('should have any method', () => {
|
|
83
|
+
expect(approve.any).toBeDefined()
|
|
84
|
+
expect(typeof approve.any).toBe('function')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('should have all method', () => {
|
|
88
|
+
expect(approve.all).toBeDefined()
|
|
89
|
+
expect(typeof approve.all).toBe('function')
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe('Basic Approval Flow', () => {
|
|
94
|
+
it('should create approval request', async () => {
|
|
95
|
+
const result = await approve('Expense: $500 for AWS services', manager, { via: 'slack' })
|
|
96
|
+
|
|
97
|
+
expect(result).toBeDefined()
|
|
98
|
+
expect(typeof result.approved).toBe('boolean')
|
|
99
|
+
expect(result.approvedBy).toBeDefined()
|
|
100
|
+
expect(result.approvedBy?.id).toBe('manager')
|
|
101
|
+
expect(result.via).toBe('slack')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('should include approval timestamp', async () => {
|
|
105
|
+
const result = await approve('Deploy to production', manager, { via: 'email' })
|
|
106
|
+
|
|
107
|
+
expect(result.approvedAt).toBeDefined()
|
|
108
|
+
expect(result.approvedAt).toBeInstanceOf(Date)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('should resolve team to lead for approval', async () => {
|
|
112
|
+
const result = await approve('Budget increase', approvalTeam, { via: 'slack' })
|
|
113
|
+
|
|
114
|
+
expect(result).toBeDefined()
|
|
115
|
+
expect(result.approvedBy?.id).toBe('manager') // Should be team lead
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('should throw for string target without contacts', async () => {
|
|
119
|
+
// String targets don't have contacts configured, so this should throw
|
|
120
|
+
await expect(approve('Request', 'approver-id')).rejects.toThrow('No valid channel available')
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('should include notes in result', async () => {
|
|
124
|
+
const result = await approve('Request approval', manager, { via: 'slack' })
|
|
125
|
+
|
|
126
|
+
expect(result.notes).toBeDefined()
|
|
127
|
+
expect(typeof result.notes).toBe('string')
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('should throw error if no channel available', async () => {
|
|
131
|
+
const noChannelWorker: Worker = {
|
|
132
|
+
id: 'no-channel',
|
|
133
|
+
name: 'No Channel',
|
|
134
|
+
type: 'human',
|
|
135
|
+
status: 'available',
|
|
136
|
+
contacts: {},
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
await expect(approve('Request', noChannelWorker)).rejects.toThrow(
|
|
140
|
+
'No valid channel available'
|
|
141
|
+
)
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
describe('Approval with Context', () => {
|
|
146
|
+
it('should include context in approval request', async () => {
|
|
147
|
+
const result = await approve('Expense approval', manager, {
|
|
148
|
+
via: 'slack',
|
|
149
|
+
context: {
|
|
150
|
+
amount: 500,
|
|
151
|
+
category: 'Infrastructure',
|
|
152
|
+
vendor: 'AWS',
|
|
153
|
+
},
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
expect(result).toBeDefined()
|
|
157
|
+
expect(result.approvedBy).toBeDefined()
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('should use withContext for structured decisions', async () => {
|
|
161
|
+
const result = await approve.withContext(
|
|
162
|
+
'Migrate to new database',
|
|
163
|
+
manager,
|
|
164
|
+
{
|
|
165
|
+
pros: ['Better performance', 'Lower cost'],
|
|
166
|
+
cons: ['Migration effort', 'Learning curve'],
|
|
167
|
+
risks: ['Data loss', 'Downtime'],
|
|
168
|
+
mitigations: ['Backup plan', 'Staged rollout'],
|
|
169
|
+
},
|
|
170
|
+
{ via: 'email' }
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
expect(result).toBeDefined()
|
|
174
|
+
expect(result.approvedBy?.id).toBe('manager')
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('should include alternatives in withContext', async () => {
|
|
178
|
+
const result = await approve.withContext('Choose technology stack', manager, {
|
|
179
|
+
pros: ['Fast development'],
|
|
180
|
+
cons: ['Limited ecosystem'],
|
|
181
|
+
alternatives: ['React', 'Vue', 'Svelte'],
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
expect(result).toBeDefined()
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
describe('Batch Approvals', () => {
|
|
189
|
+
it('should handle batch approval requests', async () => {
|
|
190
|
+
const results = await approve.batch(
|
|
191
|
+
['Expense: $100 for supplies', 'Expense: $200 for software', 'Expense: $50 for books'],
|
|
192
|
+
manager,
|
|
193
|
+
{ via: 'slack' }
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
expect(results).toBeDefined()
|
|
197
|
+
expect(Array.isArray(results)).toBe(true)
|
|
198
|
+
expect(results.length).toBe(3)
|
|
199
|
+
results.forEach((result) => {
|
|
200
|
+
expect(result.approved).toBeDefined()
|
|
201
|
+
expect(result.approvedBy?.id).toBe('manager')
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
describe('Deadline-based Approvals', () => {
|
|
207
|
+
it('should support deadline option', async () => {
|
|
208
|
+
const deadline = new Date(Date.now() + 24 * 60 * 60 * 1000) // 24 hours
|
|
209
|
+
const result = await approve.withDeadline('Time-sensitive approval', manager, deadline, {
|
|
210
|
+
via: 'slack',
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
expect(result).toBeDefined()
|
|
214
|
+
expect(result.approvedBy).toBeDefined()
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('should handle past deadline gracefully', async () => {
|
|
218
|
+
const pastDeadline = new Date(Date.now() - 1000) // 1 second ago
|
|
219
|
+
const result = await approve.withDeadline('Expired deadline', manager, pastDeadline, {
|
|
220
|
+
via: 'email',
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
expect(result).toBeDefined()
|
|
224
|
+
// Should still process even with past deadline
|
|
225
|
+
})
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
describe('Multi-Approver Workflows', () => {
|
|
229
|
+
it('should support any-of approval', async () => {
|
|
230
|
+
const approvers: Worker[] = [manager, finance]
|
|
231
|
+
|
|
232
|
+
const result = await approve.any('Urgent: Production fix', approvers, { via: 'slack' })
|
|
233
|
+
|
|
234
|
+
expect(result).toBeDefined()
|
|
235
|
+
expect(result.approved).toBeDefined()
|
|
236
|
+
// First approver to respond wins
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it('should support all-of approval', async () => {
|
|
240
|
+
const approvers: Worker[] = [manager, finance]
|
|
241
|
+
|
|
242
|
+
const result = await approve.all('Major infrastructure change', approvers, { via: 'email' })
|
|
243
|
+
|
|
244
|
+
expect(result).toBeDefined()
|
|
245
|
+
expect(result.approved).toBeDefined()
|
|
246
|
+
expect(result.approvals).toBeDefined()
|
|
247
|
+
expect(Array.isArray(result.approvals)).toBe(true)
|
|
248
|
+
expect(result.approvals.length).toBe(2)
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('should track individual approvals in all-of', async () => {
|
|
252
|
+
const result = await approve.all('Multi-approval request', [manager, finance], {
|
|
253
|
+
via: 'slack',
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
expect(result.approvals).toBeDefined()
|
|
257
|
+
result.approvals.forEach((approval) => {
|
|
258
|
+
expect(approval.approved).toBeDefined()
|
|
259
|
+
expect(approval.approvedBy).toBeDefined()
|
|
260
|
+
})
|
|
261
|
+
})
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
describe('Channel Selection', () => {
|
|
265
|
+
it('should use specified channel', async () => {
|
|
266
|
+
const result = await approve('Request', manager, { via: 'email' })
|
|
267
|
+
|
|
268
|
+
expect(result.via).toBe('email')
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
it('should fall back to available channel', async () => {
|
|
272
|
+
const slackOnlyApprover: Worker = {
|
|
273
|
+
id: 'slack-only',
|
|
274
|
+
name: 'Slack Only',
|
|
275
|
+
type: 'human',
|
|
276
|
+
status: 'available',
|
|
277
|
+
contacts: {
|
|
278
|
+
slack: '@approver',
|
|
279
|
+
},
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const result = await approve('Request', slackOnlyApprover)
|
|
283
|
+
expect(result.via).toBe('slack')
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
it('should handle channel array', async () => {
|
|
287
|
+
const result = await approve('Request', manager, { via: ['slack', 'email'] })
|
|
288
|
+
|
|
289
|
+
// Should use first available
|
|
290
|
+
expect(result.via).toBe('slack')
|
|
291
|
+
})
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
describe('Escalation', () => {
|
|
295
|
+
it('should support escalate option', async () => {
|
|
296
|
+
const result = await approve('Escalated approval', manager, {
|
|
297
|
+
via: 'slack',
|
|
298
|
+
escalate: true,
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
expect(result).toBeDefined()
|
|
302
|
+
// Escalation is a hint to the approval system
|
|
303
|
+
})
|
|
304
|
+
})
|
|
305
|
+
})
|
package/test/ask.test.ts
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for ask() - Question routing primitive
|
|
3
|
+
*
|
|
4
|
+
* The ask() function routes questions to Workers (AI Agents or Humans) via
|
|
5
|
+
* communication channels (Slack, email, SMS). Unlike ai-functions.ask() which
|
|
6
|
+
* generates content for human interaction, this function routes to actual
|
|
7
|
+
* workers and waits for their responses.
|
|
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 { ask } from '../src/index.js'
|
|
15
|
+
import type { Worker, WorkerRef, WorkerTeam } from '../src/types.js'
|
|
16
|
+
|
|
17
|
+
// Skip tests if no gateway configured
|
|
18
|
+
const hasGateway = !!process.env.AI_GATEWAY_URL || !!process.env.ANTHROPIC_API_KEY
|
|
19
|
+
|
|
20
|
+
// Test fixtures
|
|
21
|
+
const alice: Worker = {
|
|
22
|
+
id: 'alice',
|
|
23
|
+
name: 'Alice',
|
|
24
|
+
type: 'human',
|
|
25
|
+
status: 'available',
|
|
26
|
+
contacts: {
|
|
27
|
+
email: 'alice@example.com',
|
|
28
|
+
slack: '@alice',
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const bob: WorkerRef = {
|
|
33
|
+
id: 'bob',
|
|
34
|
+
name: 'Bob',
|
|
35
|
+
type: 'human',
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const aiAssistant: Worker = {
|
|
39
|
+
id: 'ai-assistant',
|
|
40
|
+
name: 'AI Assistant',
|
|
41
|
+
type: 'agent',
|
|
42
|
+
status: 'available',
|
|
43
|
+
contacts: {
|
|
44
|
+
api: 'https://api.example.com/assistant',
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const engineering: WorkerTeam = {
|
|
49
|
+
id: 'eng',
|
|
50
|
+
name: 'Engineering',
|
|
51
|
+
members: [alice, bob],
|
|
52
|
+
contacts: {
|
|
53
|
+
slack: '#engineering',
|
|
54
|
+
email: 'eng@example.com',
|
|
55
|
+
},
|
|
56
|
+
lead: alice,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
describe('ask() - Question Routing Primitive', () => {
|
|
60
|
+
describe('Unit Tests (no AI)', () => {
|
|
61
|
+
it('should be exported from index', () => {
|
|
62
|
+
expect(ask).toBeDefined()
|
|
63
|
+
expect(typeof ask).toBe('function')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('should have ai method', () => {
|
|
67
|
+
expect(ask.ai).toBeDefined()
|
|
68
|
+
expect(typeof ask.ai).toBe('function')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('should have batch method', () => {
|
|
72
|
+
expect(ask.batch).toBeDefined()
|
|
73
|
+
expect(typeof ask.batch).toBe('function')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('should have clarify method', () => {
|
|
77
|
+
expect(ask.clarify).toBeDefined()
|
|
78
|
+
expect(typeof ask.clarify).toBe('function')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('should have yesNo method', () => {
|
|
82
|
+
expect(ask.yesNo).toBeDefined()
|
|
83
|
+
expect(typeof ask.yesNo).toBe('function')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('should have choose method', () => {
|
|
87
|
+
expect(ask.choose).toBeDefined()
|
|
88
|
+
expect(typeof ask.choose).toBe('function')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
// Basic routing tests (no AI needed, tests channel resolution)
|
|
92
|
+
it('should resolve worker target', async () => {
|
|
93
|
+
const result = await ask(alice, 'What is the status?', {
|
|
94
|
+
via: 'slack',
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
expect(result).toBeDefined()
|
|
98
|
+
expect(result.answeredBy).toBeDefined()
|
|
99
|
+
expect(result.answeredBy?.id).toBe('alice')
|
|
100
|
+
expect(result.via).toBe('slack')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('should resolve team target to lead', async () => {
|
|
104
|
+
const result = await ask(engineering, 'What is the sprint status?', {
|
|
105
|
+
via: 'slack',
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
expect(result).toBeDefined()
|
|
109
|
+
expect(result.answeredBy).toBeDefined()
|
|
110
|
+
// Team should route to lead (alice)
|
|
111
|
+
expect(result.answeredBy?.id).toBe('alice')
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('should throw for string target without contacts', async () => {
|
|
115
|
+
// String targets don't have contacts configured, so this should throw
|
|
116
|
+
await expect(ask('charlie', 'Hello?')).rejects.toThrow('No valid channel available')
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('should include timestamp in result', async () => {
|
|
120
|
+
const result = await ask(alice, 'What time is it?', {
|
|
121
|
+
via: 'email',
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
expect(result.answeredAt).toBeDefined()
|
|
125
|
+
expect(result.answeredAt).toBeInstanceOf(Date)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('should throw error if no channel available', async () => {
|
|
129
|
+
const noChannelWorker: Worker = {
|
|
130
|
+
id: 'no-channel',
|
|
131
|
+
name: 'No Channel',
|
|
132
|
+
type: 'human',
|
|
133
|
+
status: 'available',
|
|
134
|
+
contacts: {},
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
await expect(ask(noChannelWorker, 'Hello?')).rejects.toThrow('No valid channel available')
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
describe.skipIf(!hasGateway)('Integration Tests (with AI) - ask.ai()', () => {
|
|
142
|
+
it('should answer a simple question', async () => {
|
|
143
|
+
const answer = await ask.ai<string>('What is 2 + 2?')
|
|
144
|
+
|
|
145
|
+
expect(answer).toBeDefined()
|
|
146
|
+
expect(typeof answer).toBe('string')
|
|
147
|
+
expect(answer).toContain('4')
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('should answer with context', async () => {
|
|
151
|
+
const answer = await ask.ai<string>('What is the capital of the country?', {
|
|
152
|
+
country: 'France',
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
expect(answer).toBeDefined()
|
|
156
|
+
expect(answer.toLowerCase()).toContain('paris')
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('should answer with structured schema', async () => {
|
|
160
|
+
const answer = await ask.ai<{ city: string; population: number }>(
|
|
161
|
+
'Give me information about Tokyo',
|
|
162
|
+
undefined,
|
|
163
|
+
{
|
|
164
|
+
city: 'City name',
|
|
165
|
+
population: 'Approximate population in millions (number)',
|
|
166
|
+
}
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
expect(answer).toBeDefined()
|
|
170
|
+
expect(typeof answer.city).toBe('string')
|
|
171
|
+
expect(typeof answer.population).toBe('number')
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('should answer factual questions', async () => {
|
|
175
|
+
const answer = await ask.ai<string>('Who wrote Romeo and Juliet?')
|
|
176
|
+
|
|
177
|
+
expect(answer).toBeDefined()
|
|
178
|
+
expect(answer.toLowerCase()).toContain('shakespeare')
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('should handle complex context', async () => {
|
|
182
|
+
const answer = await ask.ai<{ recommendation: string; reason: string }>(
|
|
183
|
+
'Should I use this database?',
|
|
184
|
+
{
|
|
185
|
+
useCase: 'High-traffic e-commerce',
|
|
186
|
+
requirements: ['ACID compliance', 'Horizontal scaling'],
|
|
187
|
+
options: ['PostgreSQL', 'MongoDB', 'CockroachDB'],
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
recommendation: 'The recommended database',
|
|
191
|
+
reason: 'Why this is recommended',
|
|
192
|
+
}
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
expect(answer).toBeDefined()
|
|
196
|
+
expect(answer.recommendation).toBeDefined()
|
|
197
|
+
expect(answer.reason).toBeDefined()
|
|
198
|
+
})
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
describe('Integration Tests - Channel Selection', () => {
|
|
202
|
+
it('should prefer specified channel', async () => {
|
|
203
|
+
const result = await ask(alice, 'Question?', { via: 'email' })
|
|
204
|
+
expect(result.via).toBe('email')
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('should fallback to available channel', async () => {
|
|
208
|
+
const slackOnlyWorker: Worker = {
|
|
209
|
+
id: 'slack-only',
|
|
210
|
+
name: 'Slack Only',
|
|
211
|
+
type: 'human',
|
|
212
|
+
status: 'available',
|
|
213
|
+
contacts: {
|
|
214
|
+
slack: '@slackuser',
|
|
215
|
+
},
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const result = await ask(slackOnlyWorker, 'Question?')
|
|
219
|
+
expect(result.via).toBe('slack')
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('should handle channel array', async () => {
|
|
223
|
+
const result = await ask(alice, 'Question?', {
|
|
224
|
+
via: ['slack', 'email'],
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
// Should use first available channel in the array
|
|
228
|
+
expect(result.via).toBe('slack')
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
describe('Integration Tests - Batch and Variants', () => {
|
|
233
|
+
it('should ask multiple questions via batch', async () => {
|
|
234
|
+
const results = await ask.batch(alice, ['Question 1?', 'Question 2?', 'Question 3?'], {
|
|
235
|
+
via: 'slack',
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
expect(results).toBeDefined()
|
|
239
|
+
expect(Array.isArray(results)).toBe(true)
|
|
240
|
+
expect(results.length).toBe(3)
|
|
241
|
+
results.forEach((result) => {
|
|
242
|
+
expect(result.answeredBy).toBeDefined()
|
|
243
|
+
expect(result.via).toBe('slack')
|
|
244
|
+
})
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it('should ask for clarification', async () => {
|
|
248
|
+
const result = await ask.clarify(alice, 'the deployment process', {
|
|
249
|
+
via: 'slack',
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
expect(result).toBeDefined()
|
|
253
|
+
expect(result.answeredBy?.id).toBe('alice')
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('should ask yes/no question', async () => {
|
|
257
|
+
const result = await ask.yesNo(alice, 'Should we proceed?', {
|
|
258
|
+
via: 'email',
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
expect(result).toBeDefined()
|
|
262
|
+
expect(result.via).toBe('email')
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('should ask choice question', async () => {
|
|
266
|
+
const result = await ask.choose(alice, 'Which color?', ['Red', 'Green', 'Blue'], {
|
|
267
|
+
via: 'slack',
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
expect(result).toBeDefined()
|
|
271
|
+
expect(result.answeredBy?.id).toBe('alice')
|
|
272
|
+
})
|
|
273
|
+
})
|
|
274
|
+
})
|