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,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load Balancing Safety Tests
|
|
3
|
+
*
|
|
4
|
+
* TDD tests for ensuring proper bounds checking and null safety
|
|
5
|
+
* in load balancing operations. Replaces non-null assertions with
|
|
6
|
+
* explicit bounds checks.
|
|
7
|
+
*
|
|
8
|
+
* Issue: aip-w8mm
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, beforeEach } from 'vitest'
|
|
12
|
+
import type { AgentInfo, TaskRequest } from '../src/load-balancing.js'
|
|
13
|
+
import {
|
|
14
|
+
createRoundRobinBalancer,
|
|
15
|
+
createLeastBusyBalancer,
|
|
16
|
+
createCapabilityRouter,
|
|
17
|
+
createPriorityQueueBalancer,
|
|
18
|
+
createMetricsCollector,
|
|
19
|
+
} from '../src/load-balancing.js'
|
|
20
|
+
import type { WorkerStatus } from '../src/types.js'
|
|
21
|
+
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// Test Fixtures
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
const createAgent = (
|
|
27
|
+
id: string,
|
|
28
|
+
skills: string[] = [],
|
|
29
|
+
status: WorkerStatus = 'available',
|
|
30
|
+
currentLoad = 0
|
|
31
|
+
): AgentInfo => ({
|
|
32
|
+
id,
|
|
33
|
+
name: `Agent ${id}`,
|
|
34
|
+
type: 'agent',
|
|
35
|
+
status,
|
|
36
|
+
skills,
|
|
37
|
+
currentLoad,
|
|
38
|
+
maxLoad: 10,
|
|
39
|
+
contacts: { api: `https://agent-${id}.example.com` },
|
|
40
|
+
metadata: {},
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const createTask = (
|
|
44
|
+
id: string,
|
|
45
|
+
requiredSkills: string[] = [],
|
|
46
|
+
priority: number = 5
|
|
47
|
+
): TaskRequest => ({
|
|
48
|
+
id,
|
|
49
|
+
name: `Task ${id}`,
|
|
50
|
+
requiredSkills,
|
|
51
|
+
priority,
|
|
52
|
+
metadata: {},
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// ============================================================================
|
|
56
|
+
// Empty Agent Array Handling Tests
|
|
57
|
+
// ============================================================================
|
|
58
|
+
|
|
59
|
+
describe('Empty Agent Array Handling', () => {
|
|
60
|
+
describe('RoundRobinBalancer with empty agents', () => {
|
|
61
|
+
it('should handle empty agent array gracefully', () => {
|
|
62
|
+
const balancer = createRoundRobinBalancer([])
|
|
63
|
+
const task = createTask('task-1')
|
|
64
|
+
|
|
65
|
+
const result = balancer.route(task)
|
|
66
|
+
|
|
67
|
+
expect(result.agent).toBeNull()
|
|
68
|
+
expect(result.reason).toBe('no-available-agents')
|
|
69
|
+
expect(result.strategy).toBe('round-robin')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('should not throw when routing with zero agents', () => {
|
|
73
|
+
const balancer = createRoundRobinBalancer([])
|
|
74
|
+
|
|
75
|
+
expect(() => balancer.route(createTask('task-1'))).not.toThrow()
|
|
76
|
+
expect(() => balancer.route(createTask('task-2'))).not.toThrow()
|
|
77
|
+
expect(() => balancer.route(createTask('task-3'))).not.toThrow()
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('should handle agents being removed until empty', () => {
|
|
81
|
+
const agents = [createAgent('agent-1')]
|
|
82
|
+
const balancer = createRoundRobinBalancer(agents)
|
|
83
|
+
|
|
84
|
+
// First route works
|
|
85
|
+
const result1 = balancer.route(createTask('task-1'))
|
|
86
|
+
expect(result1.agent?.id).toBe('agent-1')
|
|
87
|
+
|
|
88
|
+
// Remove the only agent
|
|
89
|
+
balancer.removeAgent('agent-1')
|
|
90
|
+
|
|
91
|
+
// Now routing should handle empty gracefully
|
|
92
|
+
const result2 = balancer.route(createTask('task-2'))
|
|
93
|
+
expect(result2.agent).toBeNull()
|
|
94
|
+
expect(result2.reason).toBe('no-available-agents')
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
describe('LeastBusyBalancer with empty agents', () => {
|
|
99
|
+
it('should handle empty agent array gracefully', () => {
|
|
100
|
+
const balancer = createLeastBusyBalancer([])
|
|
101
|
+
const task = createTask('task-1')
|
|
102
|
+
|
|
103
|
+
const result = balancer.route(task)
|
|
104
|
+
|
|
105
|
+
expect(result.agent).toBeNull()
|
|
106
|
+
expect(result.reason).toBe('no-available-agents')
|
|
107
|
+
expect(result.strategy).toBe('least-busy')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('should not throw when sorting empty available agents', () => {
|
|
111
|
+
// All agents are at max capacity
|
|
112
|
+
const agents = [
|
|
113
|
+
createAgent('agent-1', [], 'available', 10), // maxLoad is 10
|
|
114
|
+
createAgent('agent-2', [], 'available', 10),
|
|
115
|
+
]
|
|
116
|
+
const balancer = createLeastBusyBalancer(agents)
|
|
117
|
+
|
|
118
|
+
// Should not throw when sorting an empty array after filtering
|
|
119
|
+
expect(() => balancer.route(createTask('task-1'))).not.toThrow()
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('should handle all agents being removed', () => {
|
|
123
|
+
const agents = [createAgent('agent-1')]
|
|
124
|
+
const balancer = createLeastBusyBalancer(agents)
|
|
125
|
+
|
|
126
|
+
balancer.removeAgent('agent-1')
|
|
127
|
+
|
|
128
|
+
const result = balancer.route(createTask('task-1'))
|
|
129
|
+
expect(result.agent).toBeNull()
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
describe('CapabilityRouter with empty agents', () => {
|
|
134
|
+
it('should handle empty agent array gracefully', () => {
|
|
135
|
+
const router = createCapabilityRouter([])
|
|
136
|
+
const task = createTask('task-1', ['code'])
|
|
137
|
+
|
|
138
|
+
const result = router.route(task)
|
|
139
|
+
|
|
140
|
+
expect(result.agent).toBeNull()
|
|
141
|
+
expect(result.reason).toBe('no-matching-capability')
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('should handle empty candidates after skill filtering', () => {
|
|
145
|
+
const agents = [createAgent('agent-1', ['deploy']), createAgent('agent-2', ['monitor'])]
|
|
146
|
+
const router = createCapabilityRouter(agents)
|
|
147
|
+
|
|
148
|
+
// No agent has 'code' skill
|
|
149
|
+
const result = router.route(createTask('code-task', ['code']))
|
|
150
|
+
|
|
151
|
+
expect(result.agent).toBeNull()
|
|
152
|
+
expect(result.reason).toBe('no-matching-capability')
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
describe('PriorityQueueBalancer with empty agents', () => {
|
|
157
|
+
it('should handle empty agent array gracefully', () => {
|
|
158
|
+
const balancer = createPriorityQueueBalancer([])
|
|
159
|
+
const task = createTask('task-1')
|
|
160
|
+
|
|
161
|
+
const result = balancer.route(task)
|
|
162
|
+
|
|
163
|
+
expect(result.agent).toBeNull()
|
|
164
|
+
expect(result.reason).toBe('no-available-agents')
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('should handle routeNext with empty agents', async () => {
|
|
168
|
+
const balancer = createPriorityQueueBalancer([])
|
|
169
|
+
balancer.enqueue(createTask('task-1', [], 5))
|
|
170
|
+
|
|
171
|
+
const result = await balancer.routeNext()
|
|
172
|
+
|
|
173
|
+
expect(result).not.toBeNull()
|
|
174
|
+
expect(result?.agent).toBeNull()
|
|
175
|
+
expect(result?.reason).toBe('no-available-agents')
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
// ============================================================================
|
|
181
|
+
// Single Agent Selection Tests
|
|
182
|
+
// ============================================================================
|
|
183
|
+
|
|
184
|
+
describe('Single Agent Selection', () => {
|
|
185
|
+
describe('RoundRobinBalancer with single agent', () => {
|
|
186
|
+
it('should always route to the single available agent', () => {
|
|
187
|
+
const agents = [createAgent('only-agent')]
|
|
188
|
+
const balancer = createRoundRobinBalancer(agents)
|
|
189
|
+
|
|
190
|
+
for (let i = 0; i < 5; i++) {
|
|
191
|
+
const result = balancer.route(createTask(`task-${i}`))
|
|
192
|
+
expect(result.agent?.id).toBe('only-agent')
|
|
193
|
+
}
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('should handle single offline agent', () => {
|
|
197
|
+
const agents = [createAgent('offline-agent', [], 'offline')]
|
|
198
|
+
const balancer = createRoundRobinBalancer(agents)
|
|
199
|
+
|
|
200
|
+
const result = balancer.route(createTask('task-1'))
|
|
201
|
+
|
|
202
|
+
expect(result.agent).toBeNull()
|
|
203
|
+
expect(result.reason).toBe('no-available-agents')
|
|
204
|
+
})
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
describe('LeastBusyBalancer with single agent', () => {
|
|
208
|
+
it('should route to single agent regardless of load', () => {
|
|
209
|
+
const agents = [createAgent('only-agent', [], 'available', 5)]
|
|
210
|
+
const balancer = createLeastBusyBalancer(agents)
|
|
211
|
+
|
|
212
|
+
const result = balancer.route(createTask('task-1'))
|
|
213
|
+
|
|
214
|
+
expect(result.agent?.id).toBe('only-agent')
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('should not route when single agent is at max capacity', () => {
|
|
218
|
+
const agents = [createAgent('full-agent', [], 'available', 10)]
|
|
219
|
+
const balancer = createLeastBusyBalancer(agents)
|
|
220
|
+
|
|
221
|
+
const result = balancer.route(createTask('task-1'))
|
|
222
|
+
|
|
223
|
+
expect(result.agent).toBeNull()
|
|
224
|
+
})
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
describe('CapabilityRouter with single agent', () => {
|
|
228
|
+
it('should route when single agent has required skills', () => {
|
|
229
|
+
const agents = [createAgent('skilled-agent', ['code', 'review'])]
|
|
230
|
+
const router = createCapabilityRouter(agents)
|
|
231
|
+
|
|
232
|
+
const result = router.route(createTask('code-task', ['code']))
|
|
233
|
+
|
|
234
|
+
expect(result.agent?.id).toBe('skilled-agent')
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('should not route when single agent lacks skills', () => {
|
|
238
|
+
const agents = [createAgent('wrong-skills', ['deploy'])]
|
|
239
|
+
const router = createCapabilityRouter(agents)
|
|
240
|
+
|
|
241
|
+
const result = router.route(createTask('code-task', ['code']))
|
|
242
|
+
|
|
243
|
+
expect(result.agent).toBeNull()
|
|
244
|
+
})
|
|
245
|
+
})
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
// ============================================================================
|
|
249
|
+
// Empty Queue Handling Tests
|
|
250
|
+
// ============================================================================
|
|
251
|
+
|
|
252
|
+
describe('Empty Queue Handling', () => {
|
|
253
|
+
describe('PriorityQueueBalancer empty queue', () => {
|
|
254
|
+
it('should return null for routeNext on empty queue', async () => {
|
|
255
|
+
const agents = [createAgent('agent-1')]
|
|
256
|
+
const balancer = createPriorityQueueBalancer(agents)
|
|
257
|
+
|
|
258
|
+
const result = await balancer.routeNext()
|
|
259
|
+
|
|
260
|
+
expect(result).toBeNull()
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('should return null for peek on empty queue', () => {
|
|
264
|
+
const agents = [createAgent('agent-1')]
|
|
265
|
+
const balancer = createPriorityQueueBalancer(agents)
|
|
266
|
+
|
|
267
|
+
const peeked = balancer.peek()
|
|
268
|
+
|
|
269
|
+
expect(peeked).toBeNull()
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
it('should handle queue being emptied', async () => {
|
|
273
|
+
const agents = [createAgent('agent-1')]
|
|
274
|
+
const balancer = createPriorityQueueBalancer(agents)
|
|
275
|
+
|
|
276
|
+
balancer.enqueue(createTask('task-1', [], 5))
|
|
277
|
+
|
|
278
|
+
// Dequeue the only task
|
|
279
|
+
const result1 = await balancer.routeNext()
|
|
280
|
+
expect(result1?.task.id).toBe('task-1')
|
|
281
|
+
|
|
282
|
+
// Queue is now empty
|
|
283
|
+
const result2 = await balancer.routeNext()
|
|
284
|
+
expect(result2).toBeNull()
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
it('should handle clear then routeNext', async () => {
|
|
288
|
+
const agents = [createAgent('agent-1')]
|
|
289
|
+
const balancer = createPriorityQueueBalancer(agents)
|
|
290
|
+
|
|
291
|
+
balancer.enqueue(createTask('task-1', [], 5))
|
|
292
|
+
balancer.enqueue(createTask('task-2', [], 5))
|
|
293
|
+
|
|
294
|
+
balancer.clear()
|
|
295
|
+
|
|
296
|
+
const result = await balancer.routeNext()
|
|
297
|
+
expect(result).toBeNull()
|
|
298
|
+
})
|
|
299
|
+
})
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
// ============================================================================
|
|
303
|
+
// Edge Cases with Zero-Length Arrays
|
|
304
|
+
// ============================================================================
|
|
305
|
+
|
|
306
|
+
describe('Edge Cases with Zero-Length Arrays', () => {
|
|
307
|
+
describe('Index out of bounds prevention', () => {
|
|
308
|
+
it('should handle modulo on empty array length safely', () => {
|
|
309
|
+
// This tests the fix for: const agent = agents[currentIndex % agents.length]!
|
|
310
|
+
// When agents.length is 0, this would cause division by zero
|
|
311
|
+
const balancer = createRoundRobinBalancer([])
|
|
312
|
+
|
|
313
|
+
// Route multiple times to test index incrementing
|
|
314
|
+
for (let i = 0; i < 10; i++) {
|
|
315
|
+
expect(() => balancer.route(createTask(`task-${i}`))).not.toThrow()
|
|
316
|
+
}
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
it('should handle sorted array access on empty result safely', () => {
|
|
320
|
+
// This tests the fix for: const selected = sorted[0]!
|
|
321
|
+
// When sorted is empty, this would throw
|
|
322
|
+
const balancer = createLeastBusyBalancer([])
|
|
323
|
+
|
|
324
|
+
expect(() => balancer.route(createTask('task-1'))).not.toThrow()
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
it('should handle shift on empty queue safely', async () => {
|
|
328
|
+
// This tests the fix for: const task = queue.shift()!
|
|
329
|
+
const balancer = createPriorityQueueBalancer([createAgent('agent-1')])
|
|
330
|
+
|
|
331
|
+
// Don't enqueue anything, just try to route
|
|
332
|
+
const result = await balancer.routeNext()
|
|
333
|
+
expect(result).toBeNull()
|
|
334
|
+
})
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
describe('Concurrent agent modifications', () => {
|
|
338
|
+
it('should handle agent removal during routing iteration', () => {
|
|
339
|
+
const agents = [createAgent('agent-1'), createAgent('agent-2'), createAgent('agent-3')]
|
|
340
|
+
const balancer = createRoundRobinBalancer(agents)
|
|
341
|
+
|
|
342
|
+
// Route once
|
|
343
|
+
balancer.route(createTask('task-1'))
|
|
344
|
+
|
|
345
|
+
// Remove an agent
|
|
346
|
+
balancer.removeAgent('agent-2')
|
|
347
|
+
|
|
348
|
+
// Should still work
|
|
349
|
+
expect(() => balancer.route(createTask('task-2'))).not.toThrow()
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
it('should handle all agents being removed during operation', () => {
|
|
353
|
+
const agents = [createAgent('agent-1'), createAgent('agent-2')]
|
|
354
|
+
const balancer = createRoundRobinBalancer(agents)
|
|
355
|
+
|
|
356
|
+
balancer.removeAgent('agent-1')
|
|
357
|
+
balancer.removeAgent('agent-2')
|
|
358
|
+
|
|
359
|
+
const result = balancer.route(createTask('task-1'))
|
|
360
|
+
expect(result.agent).toBeNull()
|
|
361
|
+
})
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
describe('Metrics collection with empty results', () => {
|
|
365
|
+
it('should collect metrics even with empty agent arrays', () => {
|
|
366
|
+
const collector = createMetricsCollector()
|
|
367
|
+
const balancer = createRoundRobinBalancer([], { metricsCollector: collector })
|
|
368
|
+
|
|
369
|
+
balancer.route(createTask('task-1'))
|
|
370
|
+
balancer.route(createTask('task-2'))
|
|
371
|
+
|
|
372
|
+
const metrics = collector.collect()
|
|
373
|
+
|
|
374
|
+
expect(metrics.totalRouted).toBe(2)
|
|
375
|
+
expect(metrics.failedRoutes).toBe(2)
|
|
376
|
+
})
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
describe('Return type consistency', () => {
|
|
380
|
+
it('should always return RouteResult even when no agent found', () => {
|
|
381
|
+
const balancer = createRoundRobinBalancer([])
|
|
382
|
+
const task = createTask('task-1')
|
|
383
|
+
|
|
384
|
+
const result = balancer.route(task)
|
|
385
|
+
|
|
386
|
+
// Verify all required properties exist
|
|
387
|
+
expect(result).toHaveProperty('agent')
|
|
388
|
+
expect(result).toHaveProperty('task')
|
|
389
|
+
expect(result).toHaveProperty('strategy')
|
|
390
|
+
expect(result).toHaveProperty('timestamp')
|
|
391
|
+
expect(result.task).toBe(task)
|
|
392
|
+
expect(result.timestamp).toBeInstanceOf(Date)
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
it('should return null agent, not undefined', () => {
|
|
396
|
+
const balancer = createRoundRobinBalancer([])
|
|
397
|
+
|
|
398
|
+
const result = balancer.route(createTask('task-1'))
|
|
399
|
+
|
|
400
|
+
expect(result.agent).toBeNull()
|
|
401
|
+
expect(result.agent).not.toBeUndefined()
|
|
402
|
+
})
|
|
403
|
+
})
|
|
404
|
+
})
|