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
|
@@ -0,0 +1,1397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for agent-to-agent communication layer
|
|
3
|
+
*
|
|
4
|
+
* TDD RED Phase: Write failing tests first
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
8
|
+
import type { Worker, WorkerRef } from '../src/types.js'
|
|
9
|
+
import {
|
|
10
|
+
// Types
|
|
11
|
+
type AgentMessage,
|
|
12
|
+
type MessageEnvelope,
|
|
13
|
+
type MessageAck,
|
|
14
|
+
type HandoffRequest,
|
|
15
|
+
type HandoffResult,
|
|
16
|
+
type CoordinationPattern,
|
|
17
|
+
// Message Bus
|
|
18
|
+
AgentMessageBus,
|
|
19
|
+
createMessageBus,
|
|
20
|
+
// Core Functions
|
|
21
|
+
sendToAgent,
|
|
22
|
+
broadcastToGroup,
|
|
23
|
+
requestFromAgent,
|
|
24
|
+
onMessage,
|
|
25
|
+
acknowledge,
|
|
26
|
+
// Coordination Patterns
|
|
27
|
+
requestResponse,
|
|
28
|
+
fanOut,
|
|
29
|
+
fanIn,
|
|
30
|
+
pipeline,
|
|
31
|
+
// Handoff Protocol
|
|
32
|
+
initiateHandoff,
|
|
33
|
+
acceptHandoff,
|
|
34
|
+
rejectHandoff,
|
|
35
|
+
completeHandoff,
|
|
36
|
+
} from '../src/agent-comms.js'
|
|
37
|
+
|
|
38
|
+
// =============================================================================
|
|
39
|
+
// Test Fixtures
|
|
40
|
+
// =============================================================================
|
|
41
|
+
|
|
42
|
+
const agentA: Worker = {
|
|
43
|
+
id: 'agent-a',
|
|
44
|
+
name: 'Agent A',
|
|
45
|
+
type: 'agent',
|
|
46
|
+
status: 'available',
|
|
47
|
+
contacts: {
|
|
48
|
+
api: { endpoint: 'https://api.internal/agent-a', auth: 'bearer' },
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const agentB: Worker = {
|
|
53
|
+
id: 'agent-b',
|
|
54
|
+
name: 'Agent B',
|
|
55
|
+
type: 'agent',
|
|
56
|
+
status: 'available',
|
|
57
|
+
contacts: {
|
|
58
|
+
api: { endpoint: 'https://api.internal/agent-b', auth: 'bearer' },
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const agentC: Worker = {
|
|
63
|
+
id: 'agent-c',
|
|
64
|
+
name: 'Agent C',
|
|
65
|
+
type: 'agent',
|
|
66
|
+
status: 'available',
|
|
67
|
+
contacts: {
|
|
68
|
+
api: { endpoint: 'https://api.internal/agent-c', auth: 'bearer' },
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const agentGroup = {
|
|
73
|
+
id: 'group-processors',
|
|
74
|
+
name: 'Processors',
|
|
75
|
+
members: [agentA, agentB, agentC],
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// =============================================================================
|
|
79
|
+
// AgentMessage Interface Tests
|
|
80
|
+
// =============================================================================
|
|
81
|
+
|
|
82
|
+
describe('AgentMessage Types', () => {
|
|
83
|
+
describe('AgentMessage interface', () => {
|
|
84
|
+
it('should have required properties', () => {
|
|
85
|
+
const message: AgentMessage = {
|
|
86
|
+
id: 'msg_001',
|
|
87
|
+
type: 'request',
|
|
88
|
+
sender: 'agent-a',
|
|
89
|
+
recipient: 'agent-b',
|
|
90
|
+
payload: { task: 'process-data' },
|
|
91
|
+
timestamp: new Date(),
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
expect(message.id).toBe('msg_001')
|
|
95
|
+
expect(message.type).toBe('request')
|
|
96
|
+
expect(message.sender).toBe('agent-a')
|
|
97
|
+
expect(message.recipient).toBe('agent-b')
|
|
98
|
+
expect(message.payload).toEqual({ task: 'process-data' })
|
|
99
|
+
expect(message.timestamp).toBeInstanceOf(Date)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('should support all message types', () => {
|
|
103
|
+
const types: AgentMessage['type'][] = [
|
|
104
|
+
'request',
|
|
105
|
+
'response',
|
|
106
|
+
'notification',
|
|
107
|
+
'handoff',
|
|
108
|
+
'ack',
|
|
109
|
+
'error',
|
|
110
|
+
]
|
|
111
|
+
expect(types).toHaveLength(6)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('should support optional properties', () => {
|
|
115
|
+
const message: AgentMessage = {
|
|
116
|
+
id: 'msg_002',
|
|
117
|
+
type: 'request',
|
|
118
|
+
sender: 'agent-a',
|
|
119
|
+
recipient: 'agent-b',
|
|
120
|
+
payload: { data: 'test' },
|
|
121
|
+
timestamp: new Date(),
|
|
122
|
+
correlationId: 'corr_001',
|
|
123
|
+
replyTo: 'agent-a',
|
|
124
|
+
ttl: 30000,
|
|
125
|
+
priority: 'high',
|
|
126
|
+
metadata: { source: 'cascade' },
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
expect(message.correlationId).toBe('corr_001')
|
|
130
|
+
expect(message.replyTo).toBe('agent-a')
|
|
131
|
+
expect(message.ttl).toBe(30000)
|
|
132
|
+
expect(message.priority).toBe('high')
|
|
133
|
+
expect(message.metadata).toEqual({ source: 'cascade' })
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
describe('MessageEnvelope interface', () => {
|
|
138
|
+
it('should wrap message with delivery metadata', () => {
|
|
139
|
+
const envelope: MessageEnvelope = {
|
|
140
|
+
message: {
|
|
141
|
+
id: 'msg_003',
|
|
142
|
+
type: 'notification',
|
|
143
|
+
sender: 'agent-a',
|
|
144
|
+
recipient: 'agent-b',
|
|
145
|
+
payload: { event: 'task-complete' },
|
|
146
|
+
timestamp: new Date(),
|
|
147
|
+
},
|
|
148
|
+
deliveryAttempts: 1,
|
|
149
|
+
firstAttemptAt: new Date(),
|
|
150
|
+
lastAttemptAt: new Date(),
|
|
151
|
+
status: 'pending',
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
expect(envelope.message.id).toBe('msg_003')
|
|
155
|
+
expect(envelope.deliveryAttempts).toBe(1)
|
|
156
|
+
expect(envelope.status).toBe('pending')
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('should track delivery status', () => {
|
|
160
|
+
const statuses: MessageEnvelope['status'][] = [
|
|
161
|
+
'pending',
|
|
162
|
+
'delivered',
|
|
163
|
+
'acknowledged',
|
|
164
|
+
'failed',
|
|
165
|
+
'expired',
|
|
166
|
+
]
|
|
167
|
+
expect(statuses).toHaveLength(5)
|
|
168
|
+
})
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
describe('MessageAck interface', () => {
|
|
172
|
+
it('should acknowledge message receipt', () => {
|
|
173
|
+
const ack: MessageAck = {
|
|
174
|
+
messageId: 'msg_001',
|
|
175
|
+
status: 'received',
|
|
176
|
+
timestamp: new Date(),
|
|
177
|
+
agentId: 'agent-b',
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
expect(ack.messageId).toBe('msg_001')
|
|
181
|
+
expect(ack.status).toBe('received')
|
|
182
|
+
expect(ack.agentId).toBe('agent-b')
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('should support processing status', () => {
|
|
186
|
+
const ack: MessageAck = {
|
|
187
|
+
messageId: 'msg_001',
|
|
188
|
+
status: 'processed',
|
|
189
|
+
timestamp: new Date(),
|
|
190
|
+
agentId: 'agent-b',
|
|
191
|
+
result: { success: true, output: 'completed' },
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
expect(ack.status).toBe('processed')
|
|
195
|
+
expect(ack.result).toEqual({ success: true, output: 'completed' })
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
// =============================================================================
|
|
201
|
+
// AgentMessageBus Tests
|
|
202
|
+
// =============================================================================
|
|
203
|
+
|
|
204
|
+
describe('AgentMessageBus', () => {
|
|
205
|
+
let bus: AgentMessageBus
|
|
206
|
+
|
|
207
|
+
beforeEach(() => {
|
|
208
|
+
bus = createMessageBus()
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
afterEach(() => {
|
|
212
|
+
bus.dispose()
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
describe('creation and disposal', () => {
|
|
216
|
+
it('should create a message bus', () => {
|
|
217
|
+
expect(bus).toBeDefined()
|
|
218
|
+
expect(bus.send).toBeInstanceOf(Function)
|
|
219
|
+
expect(bus.subscribe).toBeInstanceOf(Function)
|
|
220
|
+
expect(bus.dispose).toBeInstanceOf(Function)
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('should clean up on dispose', () => {
|
|
224
|
+
const handler = vi.fn()
|
|
225
|
+
bus.subscribe('agent-a', handler)
|
|
226
|
+
bus.dispose()
|
|
227
|
+
|
|
228
|
+
// Should not call handler after dispose
|
|
229
|
+
bus.send({
|
|
230
|
+
id: 'msg_test',
|
|
231
|
+
type: 'notification',
|
|
232
|
+
sender: 'agent-b',
|
|
233
|
+
recipient: 'agent-a',
|
|
234
|
+
payload: {},
|
|
235
|
+
timestamp: new Date(),
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
expect(handler).not.toHaveBeenCalled()
|
|
239
|
+
})
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
describe('message sending', () => {
|
|
243
|
+
it('should send message to subscribed agent', async () => {
|
|
244
|
+
const handler = vi.fn()
|
|
245
|
+
bus.subscribe('agent-b', handler)
|
|
246
|
+
|
|
247
|
+
const message: AgentMessage = {
|
|
248
|
+
id: 'msg_004',
|
|
249
|
+
type: 'notification',
|
|
250
|
+
sender: 'agent-a',
|
|
251
|
+
recipient: 'agent-b',
|
|
252
|
+
payload: { event: 'test' },
|
|
253
|
+
timestamp: new Date(),
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
await bus.send(message)
|
|
257
|
+
|
|
258
|
+
expect(handler).toHaveBeenCalledWith(message)
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it('should return delivery envelope', async () => {
|
|
262
|
+
bus.subscribe('agent-b', vi.fn())
|
|
263
|
+
|
|
264
|
+
const message: AgentMessage = {
|
|
265
|
+
id: 'msg_005',
|
|
266
|
+
type: 'request',
|
|
267
|
+
sender: 'agent-a',
|
|
268
|
+
recipient: 'agent-b',
|
|
269
|
+
payload: { task: 'process' },
|
|
270
|
+
timestamp: new Date(),
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const envelope = await bus.send(message)
|
|
274
|
+
|
|
275
|
+
expect(envelope.message.id).toBe('msg_005')
|
|
276
|
+
expect(envelope.status).toBe('delivered')
|
|
277
|
+
expect(envelope.deliveryAttempts).toBe(1)
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it('should fail for unsubscribed recipient', async () => {
|
|
281
|
+
const message: AgentMessage = {
|
|
282
|
+
id: 'msg_006',
|
|
283
|
+
type: 'notification',
|
|
284
|
+
sender: 'agent-a',
|
|
285
|
+
recipient: 'unknown-agent',
|
|
286
|
+
payload: {},
|
|
287
|
+
timestamp: new Date(),
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const envelope = await bus.send(message)
|
|
291
|
+
|
|
292
|
+
expect(envelope.status).toBe('failed')
|
|
293
|
+
expect(envelope.error).toContain('not found')
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
it('should queue messages when recipient is busy', async () => {
|
|
297
|
+
let resolveHandler: () => void
|
|
298
|
+
const handlerPromise = new Promise<void>((resolve) => {
|
|
299
|
+
resolveHandler = resolve
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
// Slow handler that blocks
|
|
303
|
+
const slowHandler = vi.fn(async () => {
|
|
304
|
+
await handlerPromise
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
bus.subscribe('agent-b', slowHandler)
|
|
308
|
+
|
|
309
|
+
const msg1: AgentMessage = {
|
|
310
|
+
id: 'msg_007',
|
|
311
|
+
type: 'request',
|
|
312
|
+
sender: 'agent-a',
|
|
313
|
+
recipient: 'agent-b',
|
|
314
|
+
payload: { order: 1 },
|
|
315
|
+
timestamp: new Date(),
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const msg2: AgentMessage = {
|
|
319
|
+
id: 'msg_008',
|
|
320
|
+
type: 'request',
|
|
321
|
+
sender: 'agent-a',
|
|
322
|
+
recipient: 'agent-b',
|
|
323
|
+
payload: { order: 2 },
|
|
324
|
+
timestamp: new Date(),
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Send both messages
|
|
328
|
+
const p1 = bus.send(msg1)
|
|
329
|
+
const p2 = bus.send(msg2)
|
|
330
|
+
|
|
331
|
+
// First message should be processing
|
|
332
|
+
expect(slowHandler).toHaveBeenCalledTimes(1)
|
|
333
|
+
|
|
334
|
+
// Release the handler
|
|
335
|
+
resolveHandler!()
|
|
336
|
+
|
|
337
|
+
await Promise.all([p1, p2])
|
|
338
|
+
|
|
339
|
+
// Both should have been processed
|
|
340
|
+
expect(slowHandler).toHaveBeenCalledTimes(2)
|
|
341
|
+
})
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
describe('message subscription', () => {
|
|
345
|
+
it('should allow multiple subscribers for same agent', async () => {
|
|
346
|
+
const handler1 = vi.fn()
|
|
347
|
+
const handler2 = vi.fn()
|
|
348
|
+
|
|
349
|
+
bus.subscribe('agent-a', handler1)
|
|
350
|
+
bus.subscribe('agent-a', handler2)
|
|
351
|
+
|
|
352
|
+
const message: AgentMessage = {
|
|
353
|
+
id: 'msg_009',
|
|
354
|
+
type: 'notification',
|
|
355
|
+
sender: 'agent-b',
|
|
356
|
+
recipient: 'agent-a',
|
|
357
|
+
payload: {},
|
|
358
|
+
timestamp: new Date(),
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
await bus.send(message)
|
|
362
|
+
|
|
363
|
+
expect(handler1).toHaveBeenCalledWith(message)
|
|
364
|
+
expect(handler2).toHaveBeenCalledWith(message)
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
it('should return unsubscribe function', async () => {
|
|
368
|
+
const handler = vi.fn()
|
|
369
|
+
const unsubscribe = bus.subscribe('agent-a', handler)
|
|
370
|
+
|
|
371
|
+
unsubscribe()
|
|
372
|
+
|
|
373
|
+
await bus.send({
|
|
374
|
+
id: 'msg_010',
|
|
375
|
+
type: 'notification',
|
|
376
|
+
sender: 'agent-b',
|
|
377
|
+
recipient: 'agent-a',
|
|
378
|
+
payload: {},
|
|
379
|
+
timestamp: new Date(),
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
expect(handler).not.toHaveBeenCalled()
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
it('should filter by message type', async () => {
|
|
386
|
+
const requestHandler = vi.fn()
|
|
387
|
+
const notificationHandler = vi.fn()
|
|
388
|
+
|
|
389
|
+
bus.subscribe('agent-a', requestHandler, { types: ['request'] })
|
|
390
|
+
bus.subscribe('agent-a', notificationHandler, { types: ['notification'] })
|
|
391
|
+
|
|
392
|
+
const request: AgentMessage = {
|
|
393
|
+
id: 'msg_011',
|
|
394
|
+
type: 'request',
|
|
395
|
+
sender: 'agent-b',
|
|
396
|
+
recipient: 'agent-a',
|
|
397
|
+
payload: {},
|
|
398
|
+
timestamp: new Date(),
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const notification: AgentMessage = {
|
|
402
|
+
id: 'msg_012',
|
|
403
|
+
type: 'notification',
|
|
404
|
+
sender: 'agent-b',
|
|
405
|
+
recipient: 'agent-a',
|
|
406
|
+
payload: {},
|
|
407
|
+
timestamp: new Date(),
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
await bus.send(request)
|
|
411
|
+
await bus.send(notification)
|
|
412
|
+
|
|
413
|
+
expect(requestHandler).toHaveBeenCalledTimes(1)
|
|
414
|
+
expect(notificationHandler).toHaveBeenCalledTimes(1)
|
|
415
|
+
})
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
describe('message acknowledgment', () => {
|
|
419
|
+
it('should track pending acknowledgments', async () => {
|
|
420
|
+
bus.subscribe('agent-b', vi.fn())
|
|
421
|
+
|
|
422
|
+
const message: AgentMessage = {
|
|
423
|
+
id: 'msg_013',
|
|
424
|
+
type: 'request',
|
|
425
|
+
sender: 'agent-a',
|
|
426
|
+
recipient: 'agent-b',
|
|
427
|
+
payload: {},
|
|
428
|
+
timestamp: new Date(),
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
await bus.send(message)
|
|
432
|
+
|
|
433
|
+
const pending = bus.getPendingAcks('agent-a')
|
|
434
|
+
expect(pending).toContain('msg_013')
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
it('should acknowledge received message', async () => {
|
|
438
|
+
bus.subscribe('agent-b', vi.fn())
|
|
439
|
+
|
|
440
|
+
const message: AgentMessage = {
|
|
441
|
+
id: 'msg_014',
|
|
442
|
+
type: 'request',
|
|
443
|
+
sender: 'agent-a',
|
|
444
|
+
recipient: 'agent-b',
|
|
445
|
+
payload: {},
|
|
446
|
+
timestamp: new Date(),
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
await bus.send(message)
|
|
450
|
+
await bus.acknowledge('msg_014', 'agent-b', 'received')
|
|
451
|
+
|
|
452
|
+
const pending = bus.getPendingAcks('agent-a')
|
|
453
|
+
expect(pending).not.toContain('msg_014')
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
it('should timeout unacknowledged messages', async () => {
|
|
457
|
+
vi.useFakeTimers()
|
|
458
|
+
|
|
459
|
+
bus.subscribe('agent-b', vi.fn())
|
|
460
|
+
|
|
461
|
+
const message: AgentMessage = {
|
|
462
|
+
id: 'msg_015',
|
|
463
|
+
type: 'request',
|
|
464
|
+
sender: 'agent-a',
|
|
465
|
+
recipient: 'agent-b',
|
|
466
|
+
payload: {},
|
|
467
|
+
timestamp: new Date(),
|
|
468
|
+
ttl: 1000, // 1 second TTL
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const envelope = await bus.send(message)
|
|
472
|
+
expect(envelope.status).toBe('delivered')
|
|
473
|
+
|
|
474
|
+
// Advance time past TTL
|
|
475
|
+
vi.advanceTimersByTime(2000)
|
|
476
|
+
|
|
477
|
+
const status = bus.getMessageStatus('msg_015')
|
|
478
|
+
expect(status).toBe('expired')
|
|
479
|
+
|
|
480
|
+
vi.useRealTimers()
|
|
481
|
+
})
|
|
482
|
+
})
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
// =============================================================================
|
|
486
|
+
// Core Communication Functions
|
|
487
|
+
// =============================================================================
|
|
488
|
+
|
|
489
|
+
describe('Core Communication Functions', () => {
|
|
490
|
+
let bus: AgentMessageBus
|
|
491
|
+
|
|
492
|
+
beforeEach(() => {
|
|
493
|
+
bus = createMessageBus()
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
afterEach(() => {
|
|
497
|
+
bus.dispose()
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
describe('sendToAgent', () => {
|
|
501
|
+
it('should send message to specific agent', async () => {
|
|
502
|
+
const handler = vi.fn()
|
|
503
|
+
bus.subscribe('agent-b', handler)
|
|
504
|
+
|
|
505
|
+
const envelope = await sendToAgent(bus, 'agent-a', 'agent-b', {
|
|
506
|
+
task: 'process-data',
|
|
507
|
+
input: { value: 42 },
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
expect(envelope.status).toBe('delivered')
|
|
511
|
+
expect(handler).toHaveBeenCalledWith(
|
|
512
|
+
expect.objectContaining({
|
|
513
|
+
sender: 'agent-a',
|
|
514
|
+
recipient: 'agent-b',
|
|
515
|
+
payload: { task: 'process-data', input: { value: 42 } },
|
|
516
|
+
})
|
|
517
|
+
)
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
it('should generate unique message ID', async () => {
|
|
521
|
+
bus.subscribe('agent-b', vi.fn())
|
|
522
|
+
|
|
523
|
+
const env1 = await sendToAgent(bus, 'agent-a', 'agent-b', { n: 1 })
|
|
524
|
+
const env2 = await sendToAgent(bus, 'agent-a', 'agent-b', { n: 2 })
|
|
525
|
+
|
|
526
|
+
expect(env1.message.id).not.toBe(env2.message.id)
|
|
527
|
+
})
|
|
528
|
+
|
|
529
|
+
it('should support message options', async () => {
|
|
530
|
+
const handler = vi.fn()
|
|
531
|
+
bus.subscribe('agent-b', handler)
|
|
532
|
+
|
|
533
|
+
await sendToAgent(bus, 'agent-a', 'agent-b', { data: 'test' }, {
|
|
534
|
+
priority: 'high',
|
|
535
|
+
ttl: 5000,
|
|
536
|
+
correlationId: 'corr_123',
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
expect(handler).toHaveBeenCalledWith(
|
|
540
|
+
expect.objectContaining({
|
|
541
|
+
priority: 'high',
|
|
542
|
+
ttl: 5000,
|
|
543
|
+
correlationId: 'corr_123',
|
|
544
|
+
})
|
|
545
|
+
)
|
|
546
|
+
})
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
describe('broadcastToGroup', () => {
|
|
550
|
+
it('should send to all agents in group', async () => {
|
|
551
|
+
const handlerA = vi.fn()
|
|
552
|
+
const handlerB = vi.fn()
|
|
553
|
+
const handlerC = vi.fn()
|
|
554
|
+
|
|
555
|
+
bus.subscribe('agent-a', handlerA)
|
|
556
|
+
bus.subscribe('agent-b', handlerB)
|
|
557
|
+
bus.subscribe('agent-c', handlerC)
|
|
558
|
+
|
|
559
|
+
const results = await broadcastToGroup(
|
|
560
|
+
bus,
|
|
561
|
+
'broadcaster',
|
|
562
|
+
['agent-a', 'agent-b', 'agent-c'],
|
|
563
|
+
{ announcement: 'hello all' }
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
expect(results).toHaveLength(3)
|
|
567
|
+
expect(handlerA).toHaveBeenCalled()
|
|
568
|
+
expect(handlerB).toHaveBeenCalled()
|
|
569
|
+
expect(handlerC).toHaveBeenCalled()
|
|
570
|
+
})
|
|
571
|
+
|
|
572
|
+
it('should continue on partial failure', async () => {
|
|
573
|
+
bus.subscribe('agent-a', vi.fn())
|
|
574
|
+
// agent-b not subscribed - will fail
|
|
575
|
+
bus.subscribe('agent-c', vi.fn())
|
|
576
|
+
|
|
577
|
+
const results = await broadcastToGroup(
|
|
578
|
+
bus,
|
|
579
|
+
'broadcaster',
|
|
580
|
+
['agent-a', 'agent-b', 'agent-c'],
|
|
581
|
+
{ message: 'test' }
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
expect(results).toHaveLength(3)
|
|
585
|
+
expect(results.filter((r) => r.status === 'delivered')).toHaveLength(2)
|
|
586
|
+
expect(results.filter((r) => r.status === 'failed')).toHaveLength(1)
|
|
587
|
+
})
|
|
588
|
+
|
|
589
|
+
it('should share same correlationId for batch', async () => {
|
|
590
|
+
const receivedMessages: AgentMessage[] = []
|
|
591
|
+
|
|
592
|
+
bus.subscribe('agent-a', (msg) => receivedMessages.push(msg))
|
|
593
|
+
bus.subscribe('agent-b', (msg) => receivedMessages.push(msg))
|
|
594
|
+
|
|
595
|
+
await broadcastToGroup(
|
|
596
|
+
bus,
|
|
597
|
+
'broadcaster',
|
|
598
|
+
['agent-a', 'agent-b'],
|
|
599
|
+
{ data: 'shared' }
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
expect(receivedMessages[0].correlationId).toBe(
|
|
603
|
+
receivedMessages[1].correlationId
|
|
604
|
+
)
|
|
605
|
+
})
|
|
606
|
+
})
|
|
607
|
+
|
|
608
|
+
describe('requestFromAgent (request/response)', () => {
|
|
609
|
+
it('should send request and await response', async () => {
|
|
610
|
+
// Agent B processes requests
|
|
611
|
+
bus.subscribe('agent-b', async (msg) => {
|
|
612
|
+
if (msg.type === 'request') {
|
|
613
|
+
await bus.send({
|
|
614
|
+
id: `resp_${msg.id}`,
|
|
615
|
+
type: 'response',
|
|
616
|
+
sender: 'agent-b',
|
|
617
|
+
recipient: msg.sender,
|
|
618
|
+
payload: { result: 'processed', input: msg.payload },
|
|
619
|
+
timestamp: new Date(),
|
|
620
|
+
correlationId: msg.id,
|
|
621
|
+
})
|
|
622
|
+
}
|
|
623
|
+
})
|
|
624
|
+
|
|
625
|
+
const response = await requestFromAgent(bus, 'agent-a', 'agent-b', {
|
|
626
|
+
action: 'compute',
|
|
627
|
+
value: 100,
|
|
628
|
+
})
|
|
629
|
+
|
|
630
|
+
expect(response.payload).toEqual({
|
|
631
|
+
result: 'processed',
|
|
632
|
+
input: { action: 'compute', value: 100 },
|
|
633
|
+
})
|
|
634
|
+
})
|
|
635
|
+
|
|
636
|
+
it('should timeout if no response', async () => {
|
|
637
|
+
bus.subscribe('agent-b', vi.fn()) // Handler doesn't respond
|
|
638
|
+
|
|
639
|
+
await expect(
|
|
640
|
+
requestFromAgent(
|
|
641
|
+
bus,
|
|
642
|
+
'agent-a',
|
|
643
|
+
'agent-b',
|
|
644
|
+
{ data: 'test' },
|
|
645
|
+
{ timeout: 100 }
|
|
646
|
+
)
|
|
647
|
+
).rejects.toThrow('timeout')
|
|
648
|
+
})
|
|
649
|
+
|
|
650
|
+
it('should handle error response', async () => {
|
|
651
|
+
bus.subscribe('agent-b', async (msg) => {
|
|
652
|
+
if (msg.type === 'request') {
|
|
653
|
+
await bus.send({
|
|
654
|
+
id: `err_${msg.id}`,
|
|
655
|
+
type: 'error',
|
|
656
|
+
sender: 'agent-b',
|
|
657
|
+
recipient: msg.sender,
|
|
658
|
+
payload: { error: 'Processing failed', code: 'ERR_PROCESS' },
|
|
659
|
+
timestamp: new Date(),
|
|
660
|
+
correlationId: msg.id,
|
|
661
|
+
})
|
|
662
|
+
}
|
|
663
|
+
})
|
|
664
|
+
|
|
665
|
+
await expect(
|
|
666
|
+
requestFromAgent(bus, 'agent-a', 'agent-b', { data: 'test' })
|
|
667
|
+
).rejects.toThrow('Processing failed')
|
|
668
|
+
})
|
|
669
|
+
})
|
|
670
|
+
|
|
671
|
+
describe('onMessage', () => {
|
|
672
|
+
it('should register handler for agent', () => {
|
|
673
|
+
const handler = vi.fn()
|
|
674
|
+
const unsubscribe = onMessage(bus, 'agent-a', handler)
|
|
675
|
+
|
|
676
|
+
expect(typeof unsubscribe).toBe('function')
|
|
677
|
+
})
|
|
678
|
+
|
|
679
|
+
it('should invoke handler on message receipt', async () => {
|
|
680
|
+
const handler = vi.fn()
|
|
681
|
+
onMessage(bus, 'agent-a', handler)
|
|
682
|
+
|
|
683
|
+
await bus.send({
|
|
684
|
+
id: 'msg_100',
|
|
685
|
+
type: 'notification',
|
|
686
|
+
sender: 'agent-b',
|
|
687
|
+
recipient: 'agent-a',
|
|
688
|
+
payload: { event: 'test' },
|
|
689
|
+
timestamp: new Date(),
|
|
690
|
+
})
|
|
691
|
+
|
|
692
|
+
expect(handler).toHaveBeenCalledWith(
|
|
693
|
+
expect.objectContaining({ id: 'msg_100' })
|
|
694
|
+
)
|
|
695
|
+
})
|
|
696
|
+
|
|
697
|
+
it('should filter by sender', async () => {
|
|
698
|
+
const handler = vi.fn()
|
|
699
|
+
onMessage(bus, 'agent-a', handler, { from: 'agent-b' })
|
|
700
|
+
|
|
701
|
+
await bus.send({
|
|
702
|
+
id: 'msg_101',
|
|
703
|
+
type: 'notification',
|
|
704
|
+
sender: 'agent-b',
|
|
705
|
+
recipient: 'agent-a',
|
|
706
|
+
payload: {},
|
|
707
|
+
timestamp: new Date(),
|
|
708
|
+
})
|
|
709
|
+
|
|
710
|
+
await bus.send({
|
|
711
|
+
id: 'msg_102',
|
|
712
|
+
type: 'notification',
|
|
713
|
+
sender: 'agent-c',
|
|
714
|
+
recipient: 'agent-a',
|
|
715
|
+
payload: {},
|
|
716
|
+
timestamp: new Date(),
|
|
717
|
+
})
|
|
718
|
+
|
|
719
|
+
expect(handler).toHaveBeenCalledTimes(1)
|
|
720
|
+
expect(handler).toHaveBeenCalledWith(
|
|
721
|
+
expect.objectContaining({ sender: 'agent-b' })
|
|
722
|
+
)
|
|
723
|
+
})
|
|
724
|
+
})
|
|
725
|
+
|
|
726
|
+
describe('acknowledge', () => {
|
|
727
|
+
it('should send acknowledgment message', async () => {
|
|
728
|
+
const ackHandler = vi.fn()
|
|
729
|
+
bus.subscribe('agent-a', ackHandler)
|
|
730
|
+
bus.subscribe('agent-b', vi.fn())
|
|
731
|
+
|
|
732
|
+
const message: AgentMessage = {
|
|
733
|
+
id: 'msg_ack_001',
|
|
734
|
+
type: 'request',
|
|
735
|
+
sender: 'agent-a',
|
|
736
|
+
recipient: 'agent-b',
|
|
737
|
+
payload: {},
|
|
738
|
+
timestamp: new Date(),
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
await bus.send(message)
|
|
742
|
+
await acknowledge(bus, message, 'received')
|
|
743
|
+
|
|
744
|
+
expect(ackHandler).toHaveBeenCalledWith(
|
|
745
|
+
expect.objectContaining({
|
|
746
|
+
type: 'ack',
|
|
747
|
+
correlationId: 'msg_ack_001',
|
|
748
|
+
payload: expect.objectContaining({ status: 'received' }),
|
|
749
|
+
})
|
|
750
|
+
)
|
|
751
|
+
})
|
|
752
|
+
|
|
753
|
+
it('should include result in processed ack', async () => {
|
|
754
|
+
const ackHandler = vi.fn()
|
|
755
|
+
bus.subscribe('agent-a', ackHandler)
|
|
756
|
+
bus.subscribe('agent-b', vi.fn())
|
|
757
|
+
|
|
758
|
+
const message: AgentMessage = {
|
|
759
|
+
id: 'msg_ack_002',
|
|
760
|
+
type: 'request',
|
|
761
|
+
sender: 'agent-a',
|
|
762
|
+
recipient: 'agent-b',
|
|
763
|
+
payload: {},
|
|
764
|
+
timestamp: new Date(),
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
await bus.send(message)
|
|
768
|
+
await acknowledge(bus, message, 'processed', { output: 'done' })
|
|
769
|
+
|
|
770
|
+
expect(ackHandler).toHaveBeenCalledWith(
|
|
771
|
+
expect.objectContaining({
|
|
772
|
+
payload: expect.objectContaining({
|
|
773
|
+
status: 'processed',
|
|
774
|
+
result: { output: 'done' },
|
|
775
|
+
}),
|
|
776
|
+
})
|
|
777
|
+
)
|
|
778
|
+
})
|
|
779
|
+
})
|
|
780
|
+
})
|
|
781
|
+
|
|
782
|
+
// =============================================================================
|
|
783
|
+
// Coordination Patterns
|
|
784
|
+
// =============================================================================
|
|
785
|
+
|
|
786
|
+
describe('Coordination Patterns', () => {
|
|
787
|
+
let bus: AgentMessageBus
|
|
788
|
+
|
|
789
|
+
beforeEach(() => {
|
|
790
|
+
bus = createMessageBus()
|
|
791
|
+
})
|
|
792
|
+
|
|
793
|
+
afterEach(() => {
|
|
794
|
+
bus.dispose()
|
|
795
|
+
})
|
|
796
|
+
|
|
797
|
+
describe('requestResponse pattern', () => {
|
|
798
|
+
it('should implement request/response with correlation', async () => {
|
|
799
|
+
// Setup responder
|
|
800
|
+
bus.subscribe('processor', async (msg) => {
|
|
801
|
+
if (msg.type === 'request') {
|
|
802
|
+
const result = (msg.payload as { value: number }).value * 2
|
|
803
|
+
await bus.send({
|
|
804
|
+
id: `resp_${msg.id}`,
|
|
805
|
+
type: 'response',
|
|
806
|
+
sender: 'processor',
|
|
807
|
+
recipient: msg.sender,
|
|
808
|
+
payload: { result },
|
|
809
|
+
timestamp: new Date(),
|
|
810
|
+
correlationId: msg.id,
|
|
811
|
+
})
|
|
812
|
+
}
|
|
813
|
+
})
|
|
814
|
+
|
|
815
|
+
const response = await requestResponse(bus, {
|
|
816
|
+
from: 'requester',
|
|
817
|
+
to: 'processor',
|
|
818
|
+
payload: { value: 21 },
|
|
819
|
+
})
|
|
820
|
+
|
|
821
|
+
expect(response.payload).toEqual({ result: 42 })
|
|
822
|
+
})
|
|
823
|
+
})
|
|
824
|
+
|
|
825
|
+
describe('fanOut pattern', () => {
|
|
826
|
+
it('should distribute work to multiple agents', async () => {
|
|
827
|
+
const results: unknown[] = []
|
|
828
|
+
|
|
829
|
+
bus.subscribe('worker-1', async (msg) => {
|
|
830
|
+
results.push({ worker: 1, input: msg.payload })
|
|
831
|
+
await bus.send({
|
|
832
|
+
id: `w1_${msg.id}`,
|
|
833
|
+
type: 'response',
|
|
834
|
+
sender: 'worker-1',
|
|
835
|
+
recipient: msg.sender,
|
|
836
|
+
payload: { processed: true, worker: 1 },
|
|
837
|
+
timestamp: new Date(),
|
|
838
|
+
correlationId: msg.correlationId,
|
|
839
|
+
})
|
|
840
|
+
})
|
|
841
|
+
|
|
842
|
+
bus.subscribe('worker-2', async (msg) => {
|
|
843
|
+
results.push({ worker: 2, input: msg.payload })
|
|
844
|
+
await bus.send({
|
|
845
|
+
id: `w2_${msg.id}`,
|
|
846
|
+
type: 'response',
|
|
847
|
+
sender: 'worker-2',
|
|
848
|
+
recipient: msg.sender,
|
|
849
|
+
payload: { processed: true, worker: 2 },
|
|
850
|
+
timestamp: new Date(),
|
|
851
|
+
correlationId: msg.correlationId,
|
|
852
|
+
})
|
|
853
|
+
})
|
|
854
|
+
|
|
855
|
+
const responses = await fanOut(bus, {
|
|
856
|
+
from: 'coordinator',
|
|
857
|
+
to: ['worker-1', 'worker-2'],
|
|
858
|
+
payload: { task: 'process-chunk' },
|
|
859
|
+
})
|
|
860
|
+
|
|
861
|
+
expect(responses).toHaveLength(2)
|
|
862
|
+
expect(results).toHaveLength(2)
|
|
863
|
+
})
|
|
864
|
+
|
|
865
|
+
it('should handle partial failures in fanOut', async () => {
|
|
866
|
+
bus.subscribe('worker-1', async (msg) => {
|
|
867
|
+
await bus.send({
|
|
868
|
+
id: `w1_${msg.id}`,
|
|
869
|
+
type: 'response',
|
|
870
|
+
sender: 'worker-1',
|
|
871
|
+
recipient: msg.sender,
|
|
872
|
+
payload: { success: true },
|
|
873
|
+
timestamp: new Date(),
|
|
874
|
+
correlationId: msg.correlationId,
|
|
875
|
+
})
|
|
876
|
+
})
|
|
877
|
+
|
|
878
|
+
// worker-2 not subscribed
|
|
879
|
+
|
|
880
|
+
const responses = await fanOut(bus, {
|
|
881
|
+
from: 'coordinator',
|
|
882
|
+
to: ['worker-1', 'worker-2'],
|
|
883
|
+
payload: { task: 'process' },
|
|
884
|
+
continueOnError: true,
|
|
885
|
+
})
|
|
886
|
+
|
|
887
|
+
expect(responses.filter((r) => r.success)).toHaveLength(1)
|
|
888
|
+
expect(responses.filter((r) => !r.success)).toHaveLength(1)
|
|
889
|
+
})
|
|
890
|
+
})
|
|
891
|
+
|
|
892
|
+
describe('fanIn pattern', () => {
|
|
893
|
+
it('should collect responses from multiple agents', async () => {
|
|
894
|
+
// Setup workers that send results
|
|
895
|
+
bus.subscribe('collector', vi.fn())
|
|
896
|
+
|
|
897
|
+
const collected = await fanIn(bus, {
|
|
898
|
+
collector: 'collector',
|
|
899
|
+
sources: ['source-1', 'source-2'],
|
|
900
|
+
timeout: 1000,
|
|
901
|
+
onSourceMessage: async (source) => {
|
|
902
|
+
// Simulate sources sending data
|
|
903
|
+
return { from: source, data: `data from ${source}` }
|
|
904
|
+
},
|
|
905
|
+
})
|
|
906
|
+
|
|
907
|
+
expect(collected).toHaveLength(2)
|
|
908
|
+
expect(collected.map((c) => c.from)).toContain('source-1')
|
|
909
|
+
expect(collected.map((c) => c.from)).toContain('source-2')
|
|
910
|
+
})
|
|
911
|
+
})
|
|
912
|
+
|
|
913
|
+
describe('pipeline pattern', () => {
|
|
914
|
+
it('should chain agents in sequence', async () => {
|
|
915
|
+
// Setup pipeline: step1 -> step2 -> step3
|
|
916
|
+
bus.subscribe('step1', async (msg) => {
|
|
917
|
+
const input = msg.payload as { value: number }
|
|
918
|
+
await bus.send({
|
|
919
|
+
id: `s1_${msg.id}`,
|
|
920
|
+
type: 'response',
|
|
921
|
+
sender: 'step1',
|
|
922
|
+
recipient: msg.sender,
|
|
923
|
+
payload: { value: input.value + 10 },
|
|
924
|
+
timestamp: new Date(),
|
|
925
|
+
correlationId: msg.id,
|
|
926
|
+
})
|
|
927
|
+
})
|
|
928
|
+
|
|
929
|
+
bus.subscribe('step2', async (msg) => {
|
|
930
|
+
const input = msg.payload as { value: number }
|
|
931
|
+
await bus.send({
|
|
932
|
+
id: `s2_${msg.id}`,
|
|
933
|
+
type: 'response',
|
|
934
|
+
sender: 'step2',
|
|
935
|
+
recipient: msg.sender,
|
|
936
|
+
payload: { value: input.value * 2 },
|
|
937
|
+
timestamp: new Date(),
|
|
938
|
+
correlationId: msg.id,
|
|
939
|
+
})
|
|
940
|
+
})
|
|
941
|
+
|
|
942
|
+
bus.subscribe('step3', async (msg) => {
|
|
943
|
+
const input = msg.payload as { value: number }
|
|
944
|
+
await bus.send({
|
|
945
|
+
id: `s3_${msg.id}`,
|
|
946
|
+
type: 'response',
|
|
947
|
+
sender: 'step3',
|
|
948
|
+
recipient: msg.sender,
|
|
949
|
+
payload: { value: input.value - 5, final: true },
|
|
950
|
+
timestamp: new Date(),
|
|
951
|
+
correlationId: msg.id,
|
|
952
|
+
})
|
|
953
|
+
})
|
|
954
|
+
|
|
955
|
+
const result = await pipeline(bus, {
|
|
956
|
+
initiator: 'orchestrator',
|
|
957
|
+
stages: ['step1', 'step2', 'step3'],
|
|
958
|
+
input: { value: 5 },
|
|
959
|
+
})
|
|
960
|
+
|
|
961
|
+
// Pipeline: 5 -> +10 = 15 -> *2 = 30 -> -5 = 25
|
|
962
|
+
expect(result.payload).toEqual({ value: 25, final: true })
|
|
963
|
+
})
|
|
964
|
+
|
|
965
|
+
it('should stop pipeline on stage failure', async () => {
|
|
966
|
+
bus.subscribe('step1', async (msg) => {
|
|
967
|
+
await bus.send({
|
|
968
|
+
id: `s1_${msg.id}`,
|
|
969
|
+
type: 'error',
|
|
970
|
+
sender: 'step1',
|
|
971
|
+
recipient: msg.sender,
|
|
972
|
+
payload: { error: 'Stage 1 failed' },
|
|
973
|
+
timestamp: new Date(),
|
|
974
|
+
correlationId: msg.id,
|
|
975
|
+
})
|
|
976
|
+
})
|
|
977
|
+
|
|
978
|
+
bus.subscribe('step2', vi.fn())
|
|
979
|
+
|
|
980
|
+
await expect(
|
|
981
|
+
pipeline(bus, {
|
|
982
|
+
initiator: 'orchestrator',
|
|
983
|
+
stages: ['step1', 'step2'],
|
|
984
|
+
input: { value: 1 },
|
|
985
|
+
})
|
|
986
|
+
).rejects.toThrow('Stage 1 failed')
|
|
987
|
+
})
|
|
988
|
+
})
|
|
989
|
+
})
|
|
990
|
+
|
|
991
|
+
// =============================================================================
|
|
992
|
+
// Handoff Protocol Tests
|
|
993
|
+
// =============================================================================
|
|
994
|
+
|
|
995
|
+
describe('Handoff Protocol', () => {
|
|
996
|
+
let bus: AgentMessageBus
|
|
997
|
+
|
|
998
|
+
beforeEach(() => {
|
|
999
|
+
bus = createMessageBus()
|
|
1000
|
+
})
|
|
1001
|
+
|
|
1002
|
+
afterEach(() => {
|
|
1003
|
+
bus.dispose()
|
|
1004
|
+
})
|
|
1005
|
+
|
|
1006
|
+
describe('HandoffRequest interface', () => {
|
|
1007
|
+
it('should define handoff request structure', () => {
|
|
1008
|
+
const request: HandoffRequest = {
|
|
1009
|
+
id: 'handoff_001',
|
|
1010
|
+
fromAgent: 'agent-a',
|
|
1011
|
+
toAgent: 'agent-b',
|
|
1012
|
+
context: {
|
|
1013
|
+
task: 'process-order',
|
|
1014
|
+
orderId: 'order_123',
|
|
1015
|
+
progress: 0.5,
|
|
1016
|
+
},
|
|
1017
|
+
reason: 'Escalation to specialist',
|
|
1018
|
+
priority: 'high',
|
|
1019
|
+
timestamp: new Date(),
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
expect(request.fromAgent).toBe('agent-a')
|
|
1023
|
+
expect(request.toAgent).toBe('agent-b')
|
|
1024
|
+
expect(request.context).toBeDefined()
|
|
1025
|
+
expect(request.reason).toBe('Escalation to specialist')
|
|
1026
|
+
})
|
|
1027
|
+
})
|
|
1028
|
+
|
|
1029
|
+
describe('initiateHandoff', () => {
|
|
1030
|
+
it('should send handoff request to target agent', async () => {
|
|
1031
|
+
const handler = vi.fn()
|
|
1032
|
+
bus.subscribe('agent-b', handler)
|
|
1033
|
+
|
|
1034
|
+
const result = await initiateHandoff(bus, {
|
|
1035
|
+
fromAgent: 'agent-a',
|
|
1036
|
+
toAgent: 'agent-b',
|
|
1037
|
+
context: { task: 'complex-analysis' },
|
|
1038
|
+
reason: 'Need specialist',
|
|
1039
|
+
})
|
|
1040
|
+
|
|
1041
|
+
expect(result.status).toBe('pending')
|
|
1042
|
+
expect(handler).toHaveBeenCalledWith(
|
|
1043
|
+
expect.objectContaining({
|
|
1044
|
+
type: 'handoff',
|
|
1045
|
+
payload: expect.objectContaining({
|
|
1046
|
+
action: 'request',
|
|
1047
|
+
context: { task: 'complex-analysis' },
|
|
1048
|
+
}),
|
|
1049
|
+
})
|
|
1050
|
+
)
|
|
1051
|
+
})
|
|
1052
|
+
|
|
1053
|
+
it('should include context in handoff', async () => {
|
|
1054
|
+
const receivedContext: unknown[] = []
|
|
1055
|
+
|
|
1056
|
+
bus.subscribe('agent-b', (msg) => {
|
|
1057
|
+
if (msg.type === 'handoff') {
|
|
1058
|
+
receivedContext.push((msg.payload as { context: unknown }).context)
|
|
1059
|
+
}
|
|
1060
|
+
})
|
|
1061
|
+
|
|
1062
|
+
await initiateHandoff(bus, {
|
|
1063
|
+
fromAgent: 'agent-a',
|
|
1064
|
+
toAgent: 'agent-b',
|
|
1065
|
+
context: {
|
|
1066
|
+
state: { step: 3, data: [1, 2, 3] },
|
|
1067
|
+
history: ['step1', 'step2'],
|
|
1068
|
+
metadata: { priority: 'high' },
|
|
1069
|
+
},
|
|
1070
|
+
})
|
|
1071
|
+
|
|
1072
|
+
expect(receivedContext[0]).toEqual({
|
|
1073
|
+
state: { step: 3, data: [1, 2, 3] },
|
|
1074
|
+
history: ['step1', 'step2'],
|
|
1075
|
+
metadata: { priority: 'high' },
|
|
1076
|
+
})
|
|
1077
|
+
})
|
|
1078
|
+
})
|
|
1079
|
+
|
|
1080
|
+
describe('acceptHandoff', () => {
|
|
1081
|
+
it('should accept pending handoff', async () => {
|
|
1082
|
+
const ackHandler = vi.fn()
|
|
1083
|
+
bus.subscribe('agent-a', ackHandler)
|
|
1084
|
+
bus.subscribe('agent-b', vi.fn())
|
|
1085
|
+
|
|
1086
|
+
const { handoffId } = await initiateHandoff(bus, {
|
|
1087
|
+
fromAgent: 'agent-a',
|
|
1088
|
+
toAgent: 'agent-b',
|
|
1089
|
+
context: {},
|
|
1090
|
+
})
|
|
1091
|
+
|
|
1092
|
+
const result = await acceptHandoff(bus, handoffId, 'agent-b')
|
|
1093
|
+
|
|
1094
|
+
expect(result.status).toBe('accepted')
|
|
1095
|
+
expect(ackHandler).toHaveBeenCalledWith(
|
|
1096
|
+
expect.objectContaining({
|
|
1097
|
+
type: 'handoff',
|
|
1098
|
+
payload: expect.objectContaining({
|
|
1099
|
+
action: 'accepted',
|
|
1100
|
+
handoffId,
|
|
1101
|
+
}),
|
|
1102
|
+
})
|
|
1103
|
+
)
|
|
1104
|
+
})
|
|
1105
|
+
|
|
1106
|
+
it('should fail to accept non-existent handoff', async () => {
|
|
1107
|
+
await expect(
|
|
1108
|
+
acceptHandoff(bus, 'non-existent', 'agent-b')
|
|
1109
|
+
).rejects.toThrow('Handoff not found')
|
|
1110
|
+
})
|
|
1111
|
+
})
|
|
1112
|
+
|
|
1113
|
+
describe('rejectHandoff', () => {
|
|
1114
|
+
it('should reject handoff with reason', async () => {
|
|
1115
|
+
const ackHandler = vi.fn()
|
|
1116
|
+
bus.subscribe('agent-a', ackHandler)
|
|
1117
|
+
bus.subscribe('agent-b', vi.fn())
|
|
1118
|
+
|
|
1119
|
+
const { handoffId } = await initiateHandoff(bus, {
|
|
1120
|
+
fromAgent: 'agent-a',
|
|
1121
|
+
toAgent: 'agent-b',
|
|
1122
|
+
context: {},
|
|
1123
|
+
})
|
|
1124
|
+
|
|
1125
|
+
const result = await rejectHandoff(bus, handoffId, 'agent-b', {
|
|
1126
|
+
reason: 'Currently at capacity',
|
|
1127
|
+
})
|
|
1128
|
+
|
|
1129
|
+
expect(result.status).toBe('rejected')
|
|
1130
|
+
expect(ackHandler).toHaveBeenCalledWith(
|
|
1131
|
+
expect.objectContaining({
|
|
1132
|
+
payload: expect.objectContaining({
|
|
1133
|
+
action: 'rejected',
|
|
1134
|
+
reason: 'Currently at capacity',
|
|
1135
|
+
}),
|
|
1136
|
+
})
|
|
1137
|
+
)
|
|
1138
|
+
})
|
|
1139
|
+
})
|
|
1140
|
+
|
|
1141
|
+
describe('completeHandoff', () => {
|
|
1142
|
+
it('should complete accepted handoff', async () => {
|
|
1143
|
+
bus.subscribe('agent-a', vi.fn())
|
|
1144
|
+
bus.subscribe('agent-b', vi.fn())
|
|
1145
|
+
|
|
1146
|
+
const { handoffId } = await initiateHandoff(bus, {
|
|
1147
|
+
fromAgent: 'agent-a',
|
|
1148
|
+
toAgent: 'agent-b',
|
|
1149
|
+
context: { task: 'process' },
|
|
1150
|
+
})
|
|
1151
|
+
|
|
1152
|
+
await acceptHandoff(bus, handoffId, 'agent-b')
|
|
1153
|
+
|
|
1154
|
+
const result = await completeHandoff(bus, handoffId, 'agent-b', {
|
|
1155
|
+
result: { success: true, output: 'completed task' },
|
|
1156
|
+
})
|
|
1157
|
+
|
|
1158
|
+
expect(result.status).toBe('completed')
|
|
1159
|
+
expect(result.result).toEqual({ success: true, output: 'completed task' })
|
|
1160
|
+
})
|
|
1161
|
+
|
|
1162
|
+
it('should fail to complete non-accepted handoff', async () => {
|
|
1163
|
+
bus.subscribe('agent-a', vi.fn())
|
|
1164
|
+
bus.subscribe('agent-b', vi.fn())
|
|
1165
|
+
|
|
1166
|
+
const { handoffId } = await initiateHandoff(bus, {
|
|
1167
|
+
fromAgent: 'agent-a',
|
|
1168
|
+
toAgent: 'agent-b',
|
|
1169
|
+
context: {},
|
|
1170
|
+
})
|
|
1171
|
+
|
|
1172
|
+
await expect(
|
|
1173
|
+
completeHandoff(bus, handoffId, 'agent-b', { result: {} })
|
|
1174
|
+
).rejects.toThrow('Handoff not accepted')
|
|
1175
|
+
})
|
|
1176
|
+
})
|
|
1177
|
+
|
|
1178
|
+
describe('handoff timeout handling', () => {
|
|
1179
|
+
it('should timeout pending handoff', async () => {
|
|
1180
|
+
vi.useFakeTimers()
|
|
1181
|
+
|
|
1182
|
+
bus.subscribe('agent-a', vi.fn())
|
|
1183
|
+
bus.subscribe('agent-b', vi.fn())
|
|
1184
|
+
|
|
1185
|
+
const { handoffId } = await initiateHandoff(bus, {
|
|
1186
|
+
fromAgent: 'agent-a',
|
|
1187
|
+
toAgent: 'agent-b',
|
|
1188
|
+
context: {},
|
|
1189
|
+
timeout: 1000,
|
|
1190
|
+
})
|
|
1191
|
+
|
|
1192
|
+
vi.advanceTimersByTime(2000)
|
|
1193
|
+
|
|
1194
|
+
const status = bus.getHandoffStatus(handoffId)
|
|
1195
|
+
expect(status).toBe('expired')
|
|
1196
|
+
|
|
1197
|
+
vi.useRealTimers()
|
|
1198
|
+
})
|
|
1199
|
+
|
|
1200
|
+
it('should notify initiator on timeout', async () => {
|
|
1201
|
+
vi.useFakeTimers()
|
|
1202
|
+
|
|
1203
|
+
const timeoutHandler = vi.fn()
|
|
1204
|
+
bus.subscribe('agent-a', timeoutHandler)
|
|
1205
|
+
bus.subscribe('agent-b', vi.fn())
|
|
1206
|
+
|
|
1207
|
+
await initiateHandoff(bus, {
|
|
1208
|
+
fromAgent: 'agent-a',
|
|
1209
|
+
toAgent: 'agent-b',
|
|
1210
|
+
context: {},
|
|
1211
|
+
timeout: 1000,
|
|
1212
|
+
onTimeout: timeoutHandler,
|
|
1213
|
+
})
|
|
1214
|
+
|
|
1215
|
+
vi.advanceTimersByTime(2000)
|
|
1216
|
+
|
|
1217
|
+
expect(timeoutHandler).toHaveBeenCalledWith(
|
|
1218
|
+
expect.objectContaining({
|
|
1219
|
+
type: 'handoff',
|
|
1220
|
+
payload: expect.objectContaining({ action: 'timeout' }),
|
|
1221
|
+
})
|
|
1222
|
+
)
|
|
1223
|
+
|
|
1224
|
+
vi.useRealTimers()
|
|
1225
|
+
})
|
|
1226
|
+
})
|
|
1227
|
+
|
|
1228
|
+
describe('handoff failure recovery', () => {
|
|
1229
|
+
it('should allow retry after rejection', async () => {
|
|
1230
|
+
bus.subscribe('agent-a', vi.fn())
|
|
1231
|
+
bus.subscribe('agent-b', vi.fn())
|
|
1232
|
+
bus.subscribe('agent-c', vi.fn())
|
|
1233
|
+
|
|
1234
|
+
// First handoff to agent-b rejected
|
|
1235
|
+
const { handoffId: firstId } = await initiateHandoff(bus, {
|
|
1236
|
+
fromAgent: 'agent-a',
|
|
1237
|
+
toAgent: 'agent-b',
|
|
1238
|
+
context: { task: 'process' },
|
|
1239
|
+
})
|
|
1240
|
+
|
|
1241
|
+
await rejectHandoff(bus, firstId, 'agent-b', { reason: 'Busy' })
|
|
1242
|
+
|
|
1243
|
+
// Retry to agent-c
|
|
1244
|
+
const { handoffId: secondId, status } = await initiateHandoff(bus, {
|
|
1245
|
+
fromAgent: 'agent-a',
|
|
1246
|
+
toAgent: 'agent-c',
|
|
1247
|
+
context: { task: 'process' },
|
|
1248
|
+
previousAttempt: firstId,
|
|
1249
|
+
})
|
|
1250
|
+
|
|
1251
|
+
expect(status).toBe('pending')
|
|
1252
|
+
expect(secondId).not.toBe(firstId)
|
|
1253
|
+
})
|
|
1254
|
+
|
|
1255
|
+
it('should track handoff history', async () => {
|
|
1256
|
+
bus.subscribe('agent-a', vi.fn())
|
|
1257
|
+
bus.subscribe('agent-b', vi.fn())
|
|
1258
|
+
bus.subscribe('agent-c', vi.fn())
|
|
1259
|
+
|
|
1260
|
+
const { handoffId: firstId } = await initiateHandoff(bus, {
|
|
1261
|
+
fromAgent: 'agent-a',
|
|
1262
|
+
toAgent: 'agent-b',
|
|
1263
|
+
context: { task: 'process' },
|
|
1264
|
+
})
|
|
1265
|
+
|
|
1266
|
+
await rejectHandoff(bus, firstId, 'agent-b', { reason: 'Busy' })
|
|
1267
|
+
|
|
1268
|
+
const { handoffId: secondId } = await initiateHandoff(bus, {
|
|
1269
|
+
fromAgent: 'agent-a',
|
|
1270
|
+
toAgent: 'agent-c',
|
|
1271
|
+
context: { task: 'process' },
|
|
1272
|
+
previousAttempt: firstId,
|
|
1273
|
+
})
|
|
1274
|
+
|
|
1275
|
+
await acceptHandoff(bus, secondId, 'agent-c')
|
|
1276
|
+
|
|
1277
|
+
const history = bus.getHandoffHistory(secondId)
|
|
1278
|
+
expect(history).toContain(firstId)
|
|
1279
|
+
})
|
|
1280
|
+
})
|
|
1281
|
+
})
|
|
1282
|
+
|
|
1283
|
+
// =============================================================================
|
|
1284
|
+
// Message Persistence Tests
|
|
1285
|
+
// =============================================================================
|
|
1286
|
+
|
|
1287
|
+
describe('Message Persistence', () => {
|
|
1288
|
+
let bus: AgentMessageBus
|
|
1289
|
+
|
|
1290
|
+
beforeEach(() => {
|
|
1291
|
+
bus = createMessageBus({ persistence: true })
|
|
1292
|
+
})
|
|
1293
|
+
|
|
1294
|
+
afterEach(() => {
|
|
1295
|
+
bus.dispose()
|
|
1296
|
+
})
|
|
1297
|
+
|
|
1298
|
+
it('should persist messages', async () => {
|
|
1299
|
+
bus.subscribe('agent-b', vi.fn())
|
|
1300
|
+
|
|
1301
|
+
await sendToAgent(bus, 'agent-a', 'agent-b', { data: 'test' })
|
|
1302
|
+
|
|
1303
|
+
const messages = bus.getStoredMessages()
|
|
1304
|
+
expect(messages.length).toBeGreaterThan(0)
|
|
1305
|
+
})
|
|
1306
|
+
|
|
1307
|
+
it('should retrieve message history', async () => {
|
|
1308
|
+
bus.subscribe('agent-b', vi.fn())
|
|
1309
|
+
|
|
1310
|
+
await sendToAgent(bus, 'agent-a', 'agent-b', { n: 1 })
|
|
1311
|
+
await sendToAgent(bus, 'agent-a', 'agent-b', { n: 2 })
|
|
1312
|
+
await sendToAgent(bus, 'agent-a', 'agent-b', { n: 3 })
|
|
1313
|
+
|
|
1314
|
+
const history = bus.getMessageHistory('agent-b', { limit: 2 })
|
|
1315
|
+
expect(history).toHaveLength(2)
|
|
1316
|
+
})
|
|
1317
|
+
|
|
1318
|
+
it('should filter messages by time range', async () => {
|
|
1319
|
+
bus.subscribe('agent-b', vi.fn())
|
|
1320
|
+
|
|
1321
|
+
const before = new Date()
|
|
1322
|
+
await sendToAgent(bus, 'agent-a', 'agent-b', { data: 'test' })
|
|
1323
|
+
const after = new Date()
|
|
1324
|
+
|
|
1325
|
+
const messages = bus.getMessageHistory('agent-b', {
|
|
1326
|
+
from: before,
|
|
1327
|
+
to: after,
|
|
1328
|
+
})
|
|
1329
|
+
|
|
1330
|
+
expect(messages.length).toBeGreaterThan(0)
|
|
1331
|
+
})
|
|
1332
|
+
|
|
1333
|
+
it('should clear old messages', async () => {
|
|
1334
|
+
bus.subscribe('agent-b', vi.fn())
|
|
1335
|
+
|
|
1336
|
+
await sendToAgent(bus, 'agent-a', 'agent-b', { data: 'test' })
|
|
1337
|
+
|
|
1338
|
+
const beforeClear = bus.getStoredMessages().length
|
|
1339
|
+
bus.clearMessages({ olderThan: new Date(Date.now() + 1000) })
|
|
1340
|
+
const afterClear = bus.getStoredMessages().length
|
|
1341
|
+
|
|
1342
|
+
expect(afterClear).toBeLessThan(beforeClear)
|
|
1343
|
+
})
|
|
1344
|
+
})
|
|
1345
|
+
|
|
1346
|
+
// =============================================================================
|
|
1347
|
+
// Integration with Worker Types
|
|
1348
|
+
// =============================================================================
|
|
1349
|
+
|
|
1350
|
+
describe('Worker Integration', () => {
|
|
1351
|
+
let bus: AgentMessageBus
|
|
1352
|
+
|
|
1353
|
+
beforeEach(() => {
|
|
1354
|
+
bus = createMessageBus()
|
|
1355
|
+
})
|
|
1356
|
+
|
|
1357
|
+
afterEach(() => {
|
|
1358
|
+
bus.dispose()
|
|
1359
|
+
})
|
|
1360
|
+
|
|
1361
|
+
it('should accept Worker as sender/recipient', async () => {
|
|
1362
|
+
bus.subscribe(agentB.id, vi.fn())
|
|
1363
|
+
|
|
1364
|
+
const envelope = await sendToAgent(bus, agentA, agentB, {
|
|
1365
|
+
task: 'analyze',
|
|
1366
|
+
})
|
|
1367
|
+
|
|
1368
|
+
expect(envelope.message.sender).toBe('agent-a')
|
|
1369
|
+
expect(envelope.message.recipient).toBe('agent-b')
|
|
1370
|
+
})
|
|
1371
|
+
|
|
1372
|
+
it('should accept WorkerRef as sender/recipient', async () => {
|
|
1373
|
+
const refA: WorkerRef = { id: 'agent-a', name: 'Agent A' }
|
|
1374
|
+
const refB: WorkerRef = { id: 'agent-b', name: 'Agent B' }
|
|
1375
|
+
|
|
1376
|
+
bus.subscribe(refB.id, vi.fn())
|
|
1377
|
+
|
|
1378
|
+
const envelope = await sendToAgent(bus, refA, refB, { data: 'test' })
|
|
1379
|
+
|
|
1380
|
+
expect(envelope.message.sender).toBe('agent-a')
|
|
1381
|
+
expect(envelope.message.recipient).toBe('agent-b')
|
|
1382
|
+
})
|
|
1383
|
+
|
|
1384
|
+
it('should work with string agent IDs', async () => {
|
|
1385
|
+
bus.subscribe('target-agent', vi.fn())
|
|
1386
|
+
|
|
1387
|
+
const envelope = await sendToAgent(
|
|
1388
|
+
bus,
|
|
1389
|
+
'source-agent',
|
|
1390
|
+
'target-agent',
|
|
1391
|
+
{ data: 'test' }
|
|
1392
|
+
)
|
|
1393
|
+
|
|
1394
|
+
expect(envelope.message.sender).toBe('source-agent')
|
|
1395
|
+
expect(envelope.message.recipient).toBe('target-agent')
|
|
1396
|
+
})
|
|
1397
|
+
})
|