digital-workers 0.1.1 → 2.0.1
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 +5 -0
- package/CHANGELOG.md +9 -0
- package/README.md +290 -106
- package/dist/actions.d.ts +95 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +437 -0
- package/dist/actions.js.map +1 -0
- package/dist/approve.d.ts +49 -0
- package/dist/approve.d.ts.map +1 -0
- package/dist/approve.js +235 -0
- package/dist/approve.js.map +1 -0
- package/dist/ask.d.ts +42 -0
- package/dist/ask.d.ts.map +1 -0
- package/dist/ask.js +227 -0
- package/dist/ask.js.map +1 -0
- package/dist/decide.d.ts +62 -0
- package/dist/decide.d.ts.map +1 -0
- package/dist/decide.js +245 -0
- package/dist/decide.js.map +1 -0
- package/dist/do.d.ts +63 -0
- package/dist/do.d.ts.map +1 -0
- package/dist/do.js +228 -0
- package/dist/do.js.map +1 -0
- package/dist/generate.d.ts +61 -0
- package/dist/generate.d.ts.map +1 -0
- package/dist/generate.js +299 -0
- package/dist/generate.js.map +1 -0
- package/dist/goals.d.ts +89 -0
- package/dist/goals.d.ts.map +1 -0
- package/dist/goals.js +206 -0
- package/dist/goals.js.map +1 -0
- package/dist/index.d.ts +68 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +69 -0
- package/dist/index.js.map +1 -0
- package/dist/is.d.ts +54 -0
- package/dist/is.d.ts.map +1 -0
- package/dist/is.js +318 -0
- package/dist/is.js.map +1 -0
- package/dist/kpis.d.ts +103 -0
- package/dist/kpis.d.ts.map +1 -0
- package/dist/kpis.js +271 -0
- package/dist/kpis.js.map +1 -0
- package/dist/notify.d.ts +47 -0
- package/dist/notify.d.ts.map +1 -0
- package/dist/notify.js +220 -0
- package/dist/notify.js.map +1 -0
- package/dist/role.d.ts +53 -0
- package/dist/role.d.ts.map +1 -0
- package/dist/role.js +111 -0
- package/dist/role.js.map +1 -0
- package/dist/team.d.ts +61 -0
- package/dist/team.d.ts.map +1 -0
- package/dist/team.js +131 -0
- package/dist/team.js.map +1 -0
- package/dist/transports.d.ts +164 -0
- package/dist/transports.d.ts.map +1 -0
- package/dist/transports.js +358 -0
- package/dist/transports.js.map +1 -0
- package/dist/types.d.ts +693 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +72 -0
- package/dist/types.js.map +1 -0
- package/package.json +27 -61
- package/src/actions.ts +615 -0
- package/src/approve.ts +317 -0
- package/src/ask.ts +304 -0
- package/src/decide.ts +295 -0
- package/src/do.ts +275 -0
- package/src/generate.ts +364 -0
- package/src/goals.ts +220 -0
- package/src/index.ts +118 -0
- package/src/is.ts +372 -0
- package/src/kpis.ts +348 -0
- package/src/notify.ts +303 -0
- package/src/role.ts +116 -0
- package/src/team.ts +142 -0
- package/src/transports.ts +504 -0
- package/src/types.ts +843 -0
- package/test/actions.test.ts +546 -0
- package/test/standalone.test.ts +299 -0
- package/test/types.test.ts +460 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for worker actions module
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
6
|
+
import {
|
|
7
|
+
handleNotify,
|
|
8
|
+
handleAsk,
|
|
9
|
+
handleApprove,
|
|
10
|
+
handleDecide,
|
|
11
|
+
handleDo,
|
|
12
|
+
registerWorkerActions,
|
|
13
|
+
withWorkers,
|
|
14
|
+
notify,
|
|
15
|
+
ask,
|
|
16
|
+
approve as approveAction,
|
|
17
|
+
decide,
|
|
18
|
+
} from '../src/actions.js'
|
|
19
|
+
import { approve } from '../src/approve.js'
|
|
20
|
+
import type {
|
|
21
|
+
Worker,
|
|
22
|
+
Team,
|
|
23
|
+
NotifyActionData,
|
|
24
|
+
AskActionData,
|
|
25
|
+
ApproveActionData,
|
|
26
|
+
DecideActionData,
|
|
27
|
+
DoActionData,
|
|
28
|
+
} from '../src/types.js'
|
|
29
|
+
|
|
30
|
+
// Mock WorkflowContext
|
|
31
|
+
const createMockContext = () => {
|
|
32
|
+
const handlers: Record<string, Record<string, unknown>> = {}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
send: vi.fn().mockResolvedValue(undefined),
|
|
36
|
+
do: vi.fn().mockImplementation(async (event, data) => {
|
|
37
|
+
// Simulate action execution
|
|
38
|
+
if (event === 'Worker.notify') {
|
|
39
|
+
return { sent: true, via: ['slack'], messageId: 'msg_123' }
|
|
40
|
+
}
|
|
41
|
+
if (event === 'Worker.ask') {
|
|
42
|
+
return { answer: 'test response', answeredAt: new Date() }
|
|
43
|
+
}
|
|
44
|
+
if (event === 'Worker.approve') {
|
|
45
|
+
return { approved: true, approvedAt: new Date() }
|
|
46
|
+
}
|
|
47
|
+
if (event === 'Worker.decide') {
|
|
48
|
+
return { choice: 'A', reasoning: 'Best option', confidence: 0.8 }
|
|
49
|
+
}
|
|
50
|
+
return {}
|
|
51
|
+
}),
|
|
52
|
+
try: vi.fn(),
|
|
53
|
+
on: new Proxy({} as Record<string, Record<string, (handler: unknown) => void>>, {
|
|
54
|
+
get: (_, namespace: string) => {
|
|
55
|
+
if (!handlers[namespace]) {
|
|
56
|
+
handlers[namespace] = {}
|
|
57
|
+
}
|
|
58
|
+
return new Proxy({}, {
|
|
59
|
+
get: (_, event: string) => {
|
|
60
|
+
return (handler: unknown) => {
|
|
61
|
+
handlers[namespace][event] = handler
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
}),
|
|
67
|
+
every: {} as any,
|
|
68
|
+
state: {},
|
|
69
|
+
log: vi.fn(),
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Test fixtures
|
|
74
|
+
const alice: Worker = {
|
|
75
|
+
id: 'alice',
|
|
76
|
+
name: 'Alice',
|
|
77
|
+
type: 'human',
|
|
78
|
+
status: 'available',
|
|
79
|
+
contacts: {
|
|
80
|
+
email: 'alice@company.com',
|
|
81
|
+
slack: { workspace: 'acme', user: 'U123' },
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const bob: Worker = {
|
|
86
|
+
id: 'bob',
|
|
87
|
+
name: 'Bob',
|
|
88
|
+
type: 'human',
|
|
89
|
+
status: 'available',
|
|
90
|
+
contacts: {
|
|
91
|
+
email: 'bob@company.com',
|
|
92
|
+
slack: { workspace: 'acme', user: 'U456' },
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const engineering: Team = {
|
|
97
|
+
id: 'team_eng',
|
|
98
|
+
name: 'Engineering',
|
|
99
|
+
members: [{ id: 'alice' }, { id: 'bob' }],
|
|
100
|
+
contacts: {
|
|
101
|
+
slack: '#engineering',
|
|
102
|
+
email: 'eng@company.com',
|
|
103
|
+
},
|
|
104
|
+
lead: { id: 'alice' },
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
describe('Action Handlers', () => {
|
|
108
|
+
let mockContext: ReturnType<typeof createMockContext>
|
|
109
|
+
|
|
110
|
+
beforeEach(() => {
|
|
111
|
+
mockContext = createMockContext()
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
describe('handleNotify', () => {
|
|
115
|
+
it('should send notification to worker', async () => {
|
|
116
|
+
const data: NotifyActionData = {
|
|
117
|
+
actor: 'system',
|
|
118
|
+
object: alice,
|
|
119
|
+
action: 'notify',
|
|
120
|
+
message: 'Hello Alice',
|
|
121
|
+
via: 'slack',
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const result = await handleNotify(data, mockContext as any)
|
|
125
|
+
|
|
126
|
+
expect(result.sent).toBe(true)
|
|
127
|
+
expect(result.via).toContain('slack')
|
|
128
|
+
expect(result.messageId).toBeDefined()
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('should send to multiple channels', async () => {
|
|
132
|
+
const data: NotifyActionData = {
|
|
133
|
+
actor: 'system',
|
|
134
|
+
object: alice,
|
|
135
|
+
action: 'notify',
|
|
136
|
+
message: 'Urgent message',
|
|
137
|
+
via: ['slack', 'email'],
|
|
138
|
+
priority: 'urgent',
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const result = await handleNotify(data, mockContext as any)
|
|
142
|
+
|
|
143
|
+
expect(result.sent).toBe(true)
|
|
144
|
+
expect(result.delivery).toBeDefined()
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('should handle missing contacts', async () => {
|
|
148
|
+
const data: NotifyActionData = {
|
|
149
|
+
actor: 'system',
|
|
150
|
+
object: { id: 'unknown' },
|
|
151
|
+
action: 'notify',
|
|
152
|
+
message: 'Test',
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const result = await handleNotify(data, mockContext as any)
|
|
156
|
+
|
|
157
|
+
expect(result.sent).toBe(false)
|
|
158
|
+
expect(result.via).toHaveLength(0)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('should emit notified event on success', async () => {
|
|
162
|
+
const data: NotifyActionData = {
|
|
163
|
+
actor: 'system',
|
|
164
|
+
object: alice,
|
|
165
|
+
action: 'notify',
|
|
166
|
+
message: 'Test',
|
|
167
|
+
via: 'slack',
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
await handleNotify(data, mockContext as any)
|
|
171
|
+
|
|
172
|
+
expect(mockContext.send).toHaveBeenCalledWith(
|
|
173
|
+
'Worker.notified',
|
|
174
|
+
expect.objectContaining({ message: 'Test' })
|
|
175
|
+
)
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
describe('handleAsk', () => {
|
|
180
|
+
it('should send question to worker', async () => {
|
|
181
|
+
const data: AskActionData = {
|
|
182
|
+
actor: 'system',
|
|
183
|
+
object: alice,
|
|
184
|
+
action: 'ask',
|
|
185
|
+
question: 'What is the status?',
|
|
186
|
+
via: 'slack',
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const result = await handleAsk(data, mockContext as any)
|
|
190
|
+
|
|
191
|
+
expect(result.answer).toBeDefined()
|
|
192
|
+
expect(result.answeredAt).toBeInstanceOf(Date)
|
|
193
|
+
expect(result.via).toBe('slack')
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('should throw if no channel available', async () => {
|
|
197
|
+
const data: AskActionData = {
|
|
198
|
+
actor: 'system',
|
|
199
|
+
object: { id: 'unknown' },
|
|
200
|
+
action: 'ask',
|
|
201
|
+
question: 'Test?',
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
await expect(handleAsk(data, mockContext as any)).rejects.toThrow(
|
|
205
|
+
'No valid channel available'
|
|
206
|
+
)
|
|
207
|
+
})
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
describe('handleApprove', () => {
|
|
211
|
+
it('should send approval request', async () => {
|
|
212
|
+
const data: ApproveActionData = {
|
|
213
|
+
actor: { id: 'system' },
|
|
214
|
+
object: alice, // Use alice which has contacts
|
|
215
|
+
action: 'approve',
|
|
216
|
+
request: 'Expense: $500',
|
|
217
|
+
via: 'slack',
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const result = await handleApprove(data, mockContext as any)
|
|
221
|
+
|
|
222
|
+
expect(result.approvedAt).toBeInstanceOf(Date)
|
|
223
|
+
expect(result.via).toBe('slack')
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('should emit approved/rejected event', async () => {
|
|
227
|
+
const data: ApproveActionData = {
|
|
228
|
+
actor: { id: 'system' },
|
|
229
|
+
object: alice, // Use alice which has contacts
|
|
230
|
+
action: 'approve',
|
|
231
|
+
request: 'Test',
|
|
232
|
+
via: 'slack',
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const result = await handleApprove(data, mockContext as any)
|
|
236
|
+
|
|
237
|
+
// Default mock returns approved: false
|
|
238
|
+
expect(mockContext.send).toHaveBeenCalledWith(
|
|
239
|
+
'Worker.rejected',
|
|
240
|
+
expect.any(Object)
|
|
241
|
+
)
|
|
242
|
+
})
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
describe('handleDecide', () => {
|
|
246
|
+
it('should make a decision', async () => {
|
|
247
|
+
const data: DecideActionData = {
|
|
248
|
+
actor: 'ai',
|
|
249
|
+
object: 'decision',
|
|
250
|
+
action: 'decide',
|
|
251
|
+
options: ['A', 'B', 'C'],
|
|
252
|
+
context: 'Choose the best option',
|
|
253
|
+
criteria: ['cost', 'time'],
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const result = await handleDecide(data, mockContext as any)
|
|
257
|
+
|
|
258
|
+
expect(result.choice).toBeDefined()
|
|
259
|
+
expect(result.reasoning).toBeDefined()
|
|
260
|
+
expect(result.confidence).toBeGreaterThan(0)
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('should emit decided event', async () => {
|
|
264
|
+
const data: DecideActionData = {
|
|
265
|
+
actor: 'ai',
|
|
266
|
+
object: 'decision',
|
|
267
|
+
action: 'decide',
|
|
268
|
+
options: ['X', 'Y'],
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
await handleDecide(data, mockContext as any)
|
|
272
|
+
|
|
273
|
+
expect(mockContext.send).toHaveBeenCalledWith(
|
|
274
|
+
'Worker.decided',
|
|
275
|
+
expect.any(Object)
|
|
276
|
+
)
|
|
277
|
+
})
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
describe('handleDo', () => {
|
|
281
|
+
it('should execute task', async () => {
|
|
282
|
+
const data: DoActionData = {
|
|
283
|
+
actor: { id: 'agent_1' },
|
|
284
|
+
object: 'production',
|
|
285
|
+
action: 'do',
|
|
286
|
+
instruction: 'Deploy v2.0',
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const result = await handleDo(data, mockContext as any)
|
|
290
|
+
|
|
291
|
+
expect(result.success).toBe(true)
|
|
292
|
+
expect(result.duration).toBeDefined()
|
|
293
|
+
expect(result.steps).toBeDefined()
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
it('should retry on failure', async () => {
|
|
297
|
+
const data: DoActionData = {
|
|
298
|
+
actor: { id: 'agent_1' },
|
|
299
|
+
object: 'task',
|
|
300
|
+
action: 'do',
|
|
301
|
+
instruction: 'Flaky task',
|
|
302
|
+
maxRetries: 2,
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const result = await handleDo(data, mockContext as any)
|
|
306
|
+
|
|
307
|
+
// Should track retry attempts in steps
|
|
308
|
+
expect(result.steps?.length).toBeGreaterThan(0)
|
|
309
|
+
})
|
|
310
|
+
})
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
describe('registerWorkerActions', () => {
|
|
314
|
+
it('should register handlers with context', () => {
|
|
315
|
+
const mockContext = createMockContext()
|
|
316
|
+
|
|
317
|
+
registerWorkerActions(mockContext as any)
|
|
318
|
+
|
|
319
|
+
// The handlers should be registered (via the proxy)
|
|
320
|
+
// We can't easily verify this without more introspection
|
|
321
|
+
expect(mockContext).toBeDefined()
|
|
322
|
+
})
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
describe('withWorkers', () => {
|
|
326
|
+
let mockContext: ReturnType<typeof createMockContext>
|
|
327
|
+
|
|
328
|
+
beforeEach(() => {
|
|
329
|
+
mockContext = createMockContext()
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
it('should extend context with notify', async () => {
|
|
333
|
+
const worker$ = withWorkers(mockContext as any)
|
|
334
|
+
|
|
335
|
+
const result = await worker$.notify(alice, 'Test message', { via: 'slack' })
|
|
336
|
+
|
|
337
|
+
expect(mockContext.do).toHaveBeenCalledWith(
|
|
338
|
+
'Worker.notify',
|
|
339
|
+
expect.objectContaining({
|
|
340
|
+
action: 'notify',
|
|
341
|
+
message: 'Test message',
|
|
342
|
+
})
|
|
343
|
+
)
|
|
344
|
+
expect(result.sent).toBe(true)
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
it('should extend context with ask', async () => {
|
|
348
|
+
const worker$ = withWorkers(mockContext as any)
|
|
349
|
+
|
|
350
|
+
const result = await worker$.ask<string>(alice, 'Question?')
|
|
351
|
+
|
|
352
|
+
expect(mockContext.do).toHaveBeenCalledWith(
|
|
353
|
+
'Worker.ask',
|
|
354
|
+
expect.objectContaining({
|
|
355
|
+
action: 'ask',
|
|
356
|
+
question: 'Question?',
|
|
357
|
+
})
|
|
358
|
+
)
|
|
359
|
+
expect(result.answer).toBeDefined()
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
it('should extend context with approve', async () => {
|
|
363
|
+
const worker$ = withWorkers(mockContext as any)
|
|
364
|
+
|
|
365
|
+
const result = await worker$.approve('Request', alice)
|
|
366
|
+
|
|
367
|
+
expect(mockContext.do).toHaveBeenCalledWith(
|
|
368
|
+
'Worker.approve',
|
|
369
|
+
expect.objectContaining({
|
|
370
|
+
action: 'approve',
|
|
371
|
+
request: 'Request',
|
|
372
|
+
})
|
|
373
|
+
)
|
|
374
|
+
expect(result.approved).toBe(true)
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
it('should extend context with decide', async () => {
|
|
378
|
+
const worker$ = withWorkers(mockContext as any)
|
|
379
|
+
|
|
380
|
+
const result = await worker$.decide({
|
|
381
|
+
options: ['A', 'B'],
|
|
382
|
+
criteria: ['speed'],
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
expect(mockContext.do).toHaveBeenCalledWith(
|
|
386
|
+
'Worker.decide',
|
|
387
|
+
expect.objectContaining({
|
|
388
|
+
action: 'decide',
|
|
389
|
+
options: ['A', 'B'],
|
|
390
|
+
})
|
|
391
|
+
)
|
|
392
|
+
expect(result.choice).toBeDefined()
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
it('should preserve original context methods', () => {
|
|
396
|
+
const worker$ = withWorkers(mockContext as any)
|
|
397
|
+
|
|
398
|
+
expect(worker$.send).toBe(mockContext.send)
|
|
399
|
+
expect(worker$.do).toBe(mockContext.do)
|
|
400
|
+
expect(worker$.state).toBe(mockContext.state)
|
|
401
|
+
expect(worker$.log).toBe(mockContext.log)
|
|
402
|
+
})
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
describe('Standalone Functions', () => {
|
|
406
|
+
describe('notify', () => {
|
|
407
|
+
it('should send notification to worker', async () => {
|
|
408
|
+
const result = await notify(alice, 'Hello', { via: 'slack' })
|
|
409
|
+
|
|
410
|
+
expect(result.sent).toBe(true)
|
|
411
|
+
expect(result.messageId).toBeDefined()
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
it('should send to team', async () => {
|
|
415
|
+
const result = await notify(engineering, 'Team update', { via: 'slack' })
|
|
416
|
+
|
|
417
|
+
expect(result.via).toContain('slack')
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
it('should handle string target', async () => {
|
|
421
|
+
const result = await notify('user_123', 'Test')
|
|
422
|
+
|
|
423
|
+
expect(result).toBeDefined()
|
|
424
|
+
})
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
describe('ask', () => {
|
|
428
|
+
it('should ask worker a question', async () => {
|
|
429
|
+
const result = await ask(alice, 'What is the status?', { via: 'slack' })
|
|
430
|
+
|
|
431
|
+
expect(result.answer).toBeDefined()
|
|
432
|
+
expect(result.answeredAt).toBeInstanceOf(Date)
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
it('should throw if no channel available', async () => {
|
|
436
|
+
await expect(ask({ id: 'unknown' }, 'Test?')).rejects.toThrow()
|
|
437
|
+
})
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
describe('approve', () => {
|
|
441
|
+
it('should request approval', async () => {
|
|
442
|
+
const result = await approve('Request', alice, { via: 'slack' })
|
|
443
|
+
|
|
444
|
+
expect(result.approvedAt).toBeInstanceOf(Date)
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
it('should support batch approval', async () => {
|
|
448
|
+
const results = await approve.batch(
|
|
449
|
+
['Request 1', 'Request 2'],
|
|
450
|
+
alice,
|
|
451
|
+
{ via: 'slack' }
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
expect(results).toHaveLength(2)
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
it('should support any approver', async () => {
|
|
458
|
+
const result = await approve.any('Urgent', [alice, bob], { via: 'slack' })
|
|
459
|
+
|
|
460
|
+
expect(result.approvedAt).toBeInstanceOf(Date)
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
it('should support all approvers', async () => {
|
|
464
|
+
const result = await approve.all('Major change', [alice, bob], { via: 'slack' })
|
|
465
|
+
|
|
466
|
+
expect(result.approvals).toBeDefined()
|
|
467
|
+
})
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
describe('decide', () => {
|
|
471
|
+
it('should make a decision', async () => {
|
|
472
|
+
const result = await decide({
|
|
473
|
+
options: ['A', 'B', 'C'],
|
|
474
|
+
context: 'Choose wisely',
|
|
475
|
+
criteria: ['cost'],
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
expect(result.choice).toBeDefined()
|
|
479
|
+
expect(result.reasoning).toBeDefined()
|
|
480
|
+
})
|
|
481
|
+
})
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
describe('Target Resolution', () => {
|
|
485
|
+
describe('Worker target', () => {
|
|
486
|
+
it('should extract contacts from worker', async () => {
|
|
487
|
+
const result = await notify(alice, 'Test', { via: 'email' })
|
|
488
|
+
|
|
489
|
+
expect(result.via).toContain('email')
|
|
490
|
+
})
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
describe('Team target', () => {
|
|
494
|
+
it('should use team contacts', async () => {
|
|
495
|
+
const result = await notify(engineering, 'Team message', { via: 'slack' })
|
|
496
|
+
|
|
497
|
+
expect(result.via).toContain('slack')
|
|
498
|
+
})
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
describe('WorkerRef target', () => {
|
|
502
|
+
it('should handle minimal reference', async () => {
|
|
503
|
+
const result = await notify({ id: 'user_1' }, 'Test')
|
|
504
|
+
|
|
505
|
+
expect(result).toBeDefined()
|
|
506
|
+
})
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
describe('String target', () => {
|
|
510
|
+
it('should handle string ID', async () => {
|
|
511
|
+
const result = await notify('user_123', 'Test')
|
|
512
|
+
|
|
513
|
+
expect(result).toBeDefined()
|
|
514
|
+
})
|
|
515
|
+
})
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
describe('Channel Resolution', () => {
|
|
519
|
+
it('should use specified channel', async () => {
|
|
520
|
+
const result = await notify(alice, 'Test', { via: 'email' })
|
|
521
|
+
|
|
522
|
+
expect(result.via).toContain('email')
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
it('should use multiple channels', async () => {
|
|
526
|
+
const result = await notify(alice, 'Urgent', {
|
|
527
|
+
via: ['slack', 'email'],
|
|
528
|
+
priority: 'urgent',
|
|
529
|
+
})
|
|
530
|
+
|
|
531
|
+
expect(result.delivery?.length).toBeGreaterThan(0)
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
it('should fallback to available channel', async () => {
|
|
535
|
+
const result = await notify(alice, 'Test')
|
|
536
|
+
|
|
537
|
+
expect(result.via.length).toBeGreaterThan(0)
|
|
538
|
+
})
|
|
539
|
+
|
|
540
|
+
it('should prioritize urgent channels', async () => {
|
|
541
|
+
const result = await notify(alice, 'Urgent!', { priority: 'urgent' })
|
|
542
|
+
|
|
543
|
+
// Should try slack/sms/phone for urgent
|
|
544
|
+
expect(result).toBeDefined()
|
|
545
|
+
})
|
|
546
|
+
})
|