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,464 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thread-Safe Metrics Tests for Load Balancing
|
|
3
|
+
*
|
|
4
|
+
* TDD tests for ensuring metrics collection is thread-safe and isolated.
|
|
5
|
+
* These tests verify that metrics can be collected per-instance without
|
|
6
|
+
* race conditions or shared state pollution.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, beforeEach } from 'vitest'
|
|
12
|
+
import type {
|
|
13
|
+
AgentInfo,
|
|
14
|
+
TaskRequest,
|
|
15
|
+
RoutingMetrics,
|
|
16
|
+
MetricsCollector,
|
|
17
|
+
} from '../src/load-balancing.js'
|
|
18
|
+
import {
|
|
19
|
+
createRoundRobinBalancer,
|
|
20
|
+
createLeastBusyBalancer,
|
|
21
|
+
createCapabilityRouter,
|
|
22
|
+
createMetricsCollector,
|
|
23
|
+
collectRoutingMetrics,
|
|
24
|
+
resetRoutingMetrics,
|
|
25
|
+
} from '../src/load-balancing.js'
|
|
26
|
+
import type { WorkerStatus } from '../src/types.js'
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// Test Fixtures
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
const createAgent = (
|
|
33
|
+
id: string,
|
|
34
|
+
skills: string[] = [],
|
|
35
|
+
status: WorkerStatus = 'available',
|
|
36
|
+
currentLoad = 0
|
|
37
|
+
): AgentInfo => ({
|
|
38
|
+
id,
|
|
39
|
+
name: `Agent ${id}`,
|
|
40
|
+
type: 'agent',
|
|
41
|
+
status,
|
|
42
|
+
skills,
|
|
43
|
+
currentLoad,
|
|
44
|
+
maxLoad: 10,
|
|
45
|
+
contacts: { api: `https://agent-${id}.example.com` },
|
|
46
|
+
metadata: {},
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const createTask = (
|
|
50
|
+
id: string,
|
|
51
|
+
requiredSkills: string[] = [],
|
|
52
|
+
priority: number = 5
|
|
53
|
+
): TaskRequest => ({
|
|
54
|
+
id,
|
|
55
|
+
name: `Task ${id}`,
|
|
56
|
+
requiredSkills,
|
|
57
|
+
priority,
|
|
58
|
+
metadata: {},
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// ============================================================================
|
|
62
|
+
// MetricsCollector Isolation Tests
|
|
63
|
+
// ============================================================================
|
|
64
|
+
|
|
65
|
+
describe('MetricsCollector Thread Safety', () => {
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
// Reset the default global metrics for clean test state
|
|
68
|
+
resetRoutingMetrics()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
describe('should isolate metrics per collector instance', () => {
|
|
72
|
+
it('two collectors should have independent state', () => {
|
|
73
|
+
const collector1 = createMetricsCollector()
|
|
74
|
+
const collector2 = createMetricsCollector()
|
|
75
|
+
|
|
76
|
+
const agents = [createAgent('agent-1')]
|
|
77
|
+
const balancer1 = createRoundRobinBalancer(agents, { metricsCollector: collector1 })
|
|
78
|
+
|
|
79
|
+
// Route 3 tasks through balancer1 (using collector1)
|
|
80
|
+
balancer1.route(createTask('t1'))
|
|
81
|
+
balancer1.route(createTask('t2'))
|
|
82
|
+
balancer1.route(createTask('t3'))
|
|
83
|
+
|
|
84
|
+
// Collector1 should have 3 routed tasks
|
|
85
|
+
const metrics1 = collector1.collect()
|
|
86
|
+
expect(metrics1.totalRouted).toBe(3)
|
|
87
|
+
|
|
88
|
+
// Collector2 should have 0 routed tasks (never used)
|
|
89
|
+
const metrics2 = collector2.collect()
|
|
90
|
+
expect(metrics2.totalRouted).toBe(0)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('recording to one collector should not affect the other', () => {
|
|
94
|
+
const collector1 = createMetricsCollector()
|
|
95
|
+
const collector2 = createMetricsCollector()
|
|
96
|
+
|
|
97
|
+
const agents = [createAgent('agent-1'), createAgent('agent-2')]
|
|
98
|
+
|
|
99
|
+
// Create balancers with different collectors
|
|
100
|
+
const balancer1 = createRoundRobinBalancer(agents, { metricsCollector: collector1 })
|
|
101
|
+
const balancer2 = createRoundRobinBalancer(agents, { metricsCollector: collector2 })
|
|
102
|
+
|
|
103
|
+
// Route tasks through balancer1
|
|
104
|
+
balancer1.route(createTask('t1'))
|
|
105
|
+
balancer1.route(createTask('t2'))
|
|
106
|
+
|
|
107
|
+
// Route tasks through balancer2
|
|
108
|
+
balancer2.route(createTask('t3'))
|
|
109
|
+
|
|
110
|
+
// Verify isolation
|
|
111
|
+
expect(collector1.collect().totalRouted).toBe(2)
|
|
112
|
+
expect(collector2.collect().totalRouted).toBe(1)
|
|
113
|
+
|
|
114
|
+
// Verify per-agent tracking is also isolated
|
|
115
|
+
expect(collector1.collect().perAgent['agent-1']?.routedCount).toBe(1)
|
|
116
|
+
expect(collector1.collect().perAgent['agent-2']?.routedCount).toBe(1)
|
|
117
|
+
expect(collector2.collect().perAgent['agent-1']?.routedCount).toBe(1)
|
|
118
|
+
expect(collector2.collect().perAgent['agent-2']).toBeUndefined()
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
describe('should support concurrent metric recording without race conditions', () => {
|
|
123
|
+
it('concurrent writes to the same collector should not lose data', async () => {
|
|
124
|
+
const collector = createMetricsCollector()
|
|
125
|
+
const agents = [createAgent('agent-1')]
|
|
126
|
+
const balancer = createRoundRobinBalancer(agents, { metricsCollector: collector })
|
|
127
|
+
|
|
128
|
+
// Simulate 100 concurrent routing operations
|
|
129
|
+
const concurrentCount = 100
|
|
130
|
+
const promises: Promise<void>[] = []
|
|
131
|
+
|
|
132
|
+
for (let i = 0; i < concurrentCount; i++) {
|
|
133
|
+
promises.push(
|
|
134
|
+
new Promise<void>((resolve) => {
|
|
135
|
+
// Small random delay to simulate real concurrency
|
|
136
|
+
setTimeout(() => {
|
|
137
|
+
balancer.route(createTask(`t${i}`))
|
|
138
|
+
resolve()
|
|
139
|
+
}, Math.random() * 10)
|
|
140
|
+
})
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
await Promise.all(promises)
|
|
145
|
+
|
|
146
|
+
// All 100 routings should be recorded
|
|
147
|
+
const metrics = collector.collect()
|
|
148
|
+
expect(metrics.totalRouted).toBe(concurrentCount)
|
|
149
|
+
expect(metrics.perAgent['agent-1']?.routedCount).toBe(concurrentCount)
|
|
150
|
+
expect(metrics.strategyUsage['round-robin']).toBe(concurrentCount)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('concurrent writes to different collectors should remain isolated', async () => {
|
|
154
|
+
const collector1 = createMetricsCollector()
|
|
155
|
+
const collector2 = createMetricsCollector()
|
|
156
|
+
|
|
157
|
+
const agents = [createAgent('agent-1')]
|
|
158
|
+
const balancer1 = createRoundRobinBalancer(agents, { metricsCollector: collector1 })
|
|
159
|
+
const balancer2 = createRoundRobinBalancer(agents, { metricsCollector: collector2 })
|
|
160
|
+
|
|
161
|
+
const count1 = 50
|
|
162
|
+
const count2 = 75
|
|
163
|
+
const promises: Promise<void>[] = []
|
|
164
|
+
|
|
165
|
+
// Concurrent writes to collector1
|
|
166
|
+
for (let i = 0; i < count1; i++) {
|
|
167
|
+
promises.push(
|
|
168
|
+
new Promise<void>((resolve) => {
|
|
169
|
+
setTimeout(() => {
|
|
170
|
+
balancer1.route(createTask(`t1-${i}`))
|
|
171
|
+
resolve()
|
|
172
|
+
}, Math.random() * 10)
|
|
173
|
+
})
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Concurrent writes to collector2
|
|
178
|
+
for (let i = 0; i < count2; i++) {
|
|
179
|
+
promises.push(
|
|
180
|
+
new Promise<void>((resolve) => {
|
|
181
|
+
setTimeout(() => {
|
|
182
|
+
balancer2.route(createTask(`t2-${i}`))
|
|
183
|
+
resolve()
|
|
184
|
+
}, Math.random() * 10)
|
|
185
|
+
})
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
await Promise.all(promises)
|
|
190
|
+
|
|
191
|
+
expect(collector1.collect().totalRouted).toBe(count1)
|
|
192
|
+
expect(collector2.collect().totalRouted).toBe(count2)
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
describe('should provide thread-safe average latency calculation', () => {
|
|
197
|
+
it('concurrent latency recordings should calculate correct average', async () => {
|
|
198
|
+
const collector = createMetricsCollector()
|
|
199
|
+
const agents = [createAgent('agent-1')]
|
|
200
|
+
const balancer = createRoundRobinBalancer(agents, { metricsCollector: collector })
|
|
201
|
+
|
|
202
|
+
// Route multiple tasks concurrently
|
|
203
|
+
const concurrentCount = 50
|
|
204
|
+
const promises: Promise<void>[] = []
|
|
205
|
+
|
|
206
|
+
for (let i = 0; i < concurrentCount; i++) {
|
|
207
|
+
promises.push(
|
|
208
|
+
new Promise<void>((resolve) => {
|
|
209
|
+
setTimeout(() => {
|
|
210
|
+
balancer.route(createTask(`t${i}`))
|
|
211
|
+
resolve()
|
|
212
|
+
}, Math.random() * 5)
|
|
213
|
+
})
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
await Promise.all(promises)
|
|
218
|
+
|
|
219
|
+
const metrics = collector.collect()
|
|
220
|
+
|
|
221
|
+
// Average latency should be a reasonable positive number
|
|
222
|
+
expect(metrics.averageLatencyMs).toBeGreaterThanOrEqual(0)
|
|
223
|
+
expect(metrics.totalRouted).toBe(concurrentCount)
|
|
224
|
+
|
|
225
|
+
// The average should be mathematically valid (totalLatency / count)
|
|
226
|
+
// We verify indirectly by ensuring it's not NaN or Infinity
|
|
227
|
+
expect(Number.isFinite(metrics.averageLatencyMs)).toBe(true)
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it('reset should properly clear latency accumulator', () => {
|
|
231
|
+
const collector = createMetricsCollector()
|
|
232
|
+
const agents = [createAgent('agent-1')]
|
|
233
|
+
const balancer = createRoundRobinBalancer(agents, { metricsCollector: collector })
|
|
234
|
+
|
|
235
|
+
// Route some tasks
|
|
236
|
+
balancer.route(createTask('t1'))
|
|
237
|
+
balancer.route(createTask('t2'))
|
|
238
|
+
|
|
239
|
+
const before = collector.collect()
|
|
240
|
+
expect(before.totalRouted).toBe(2)
|
|
241
|
+
expect(before.averageLatencyMs).toBeGreaterThanOrEqual(0)
|
|
242
|
+
|
|
243
|
+
// Reset the collector
|
|
244
|
+
collector.reset()
|
|
245
|
+
|
|
246
|
+
const after = collector.collect()
|
|
247
|
+
expect(after.totalRouted).toBe(0)
|
|
248
|
+
expect(after.averageLatencyMs).toBe(0)
|
|
249
|
+
|
|
250
|
+
// New routings should have fresh averages
|
|
251
|
+
balancer.route(createTask('t3'))
|
|
252
|
+
const final = collector.collect()
|
|
253
|
+
expect(final.totalRouted).toBe(1)
|
|
254
|
+
expect(final.averageLatencyMs).toBeGreaterThanOrEqual(0)
|
|
255
|
+
})
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
describe('should accept metrics collector in constructor', () => {
|
|
259
|
+
it('createRoundRobinBalancer should accept metricsCollector option', () => {
|
|
260
|
+
const collector = createMetricsCollector()
|
|
261
|
+
const agents = [createAgent('agent-1')]
|
|
262
|
+
|
|
263
|
+
const balancer = createRoundRobinBalancer(agents, { metricsCollector: collector })
|
|
264
|
+
|
|
265
|
+
balancer.route(createTask('t1'))
|
|
266
|
+
|
|
267
|
+
expect(collector.collect().totalRouted).toBe(1)
|
|
268
|
+
expect(collector.collect().strategyUsage['round-robin']).toBe(1)
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
it('createLeastBusyBalancer should accept metricsCollector option', () => {
|
|
272
|
+
const collector = createMetricsCollector()
|
|
273
|
+
const agents = [createAgent('agent-1')]
|
|
274
|
+
|
|
275
|
+
const balancer = createLeastBusyBalancer(agents, { metricsCollector: collector })
|
|
276
|
+
|
|
277
|
+
balancer.route(createTask('t1'))
|
|
278
|
+
|
|
279
|
+
expect(collector.collect().totalRouted).toBe(1)
|
|
280
|
+
expect(collector.collect().strategyUsage['least-busy']).toBe(1)
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
it('createCapabilityRouter should accept metricsCollector option', () => {
|
|
284
|
+
const collector = createMetricsCollector()
|
|
285
|
+
const agents = [createAgent('agent-1', ['code'])]
|
|
286
|
+
|
|
287
|
+
const router = createCapabilityRouter(agents, { metricsCollector: collector })
|
|
288
|
+
|
|
289
|
+
router.route(createTask('t1', ['code']))
|
|
290
|
+
|
|
291
|
+
expect(collector.collect().totalRouted).toBe(1)
|
|
292
|
+
expect(collector.collect().strategyUsage['capability']).toBe(1)
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
it('balancer without metricsCollector should use default global collector', () => {
|
|
296
|
+
// Reset global metrics first
|
|
297
|
+
resetRoutingMetrics()
|
|
298
|
+
|
|
299
|
+
const agents = [createAgent('agent-1')]
|
|
300
|
+
|
|
301
|
+
// Create balancer WITHOUT explicit collector (uses default)
|
|
302
|
+
const balancer = createRoundRobinBalancer(agents)
|
|
303
|
+
|
|
304
|
+
balancer.route(createTask('t1'))
|
|
305
|
+
balancer.route(createTask('t2'))
|
|
306
|
+
|
|
307
|
+
// Global metrics should be updated
|
|
308
|
+
const globalMetrics = collectRoutingMetrics()
|
|
309
|
+
expect(globalMetrics.totalRouted).toBe(2)
|
|
310
|
+
})
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
describe('should allow balancers to share metrics collector', () => {
|
|
314
|
+
it('two balancers sharing a collector should aggregate metrics', () => {
|
|
315
|
+
const sharedCollector = createMetricsCollector()
|
|
316
|
+
const agents = [createAgent('agent-1'), createAgent('agent-2')]
|
|
317
|
+
|
|
318
|
+
// Create two balancers sharing the same collector
|
|
319
|
+
const balancer1 = createRoundRobinBalancer(agents, { metricsCollector: sharedCollector })
|
|
320
|
+
const balancer2 = createLeastBusyBalancer(agents, { metricsCollector: sharedCollector })
|
|
321
|
+
|
|
322
|
+
// Route tasks through both balancers
|
|
323
|
+
balancer1.route(createTask('t1'))
|
|
324
|
+
balancer1.route(createTask('t2'))
|
|
325
|
+
balancer2.route(createTask('t3'))
|
|
326
|
+
balancer2.route(createTask('t4'))
|
|
327
|
+
balancer2.route(createTask('t5'))
|
|
328
|
+
|
|
329
|
+
const metrics = sharedCollector.collect()
|
|
330
|
+
|
|
331
|
+
// Total should be combined
|
|
332
|
+
expect(metrics.totalRouted).toBe(5)
|
|
333
|
+
|
|
334
|
+
// Strategy usage should track both
|
|
335
|
+
expect(metrics.strategyUsage['round-robin']).toBe(2)
|
|
336
|
+
expect(metrics.strategyUsage['least-busy']).toBe(3)
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
it('shared collector should track per-agent metrics from all balancers', () => {
|
|
340
|
+
const sharedCollector = createMetricsCollector()
|
|
341
|
+
|
|
342
|
+
const agents = [createAgent('agent-1'), createAgent('agent-2')]
|
|
343
|
+
|
|
344
|
+
const balancer1 = createRoundRobinBalancer(agents, { metricsCollector: sharedCollector })
|
|
345
|
+
const balancer2 = createRoundRobinBalancer(agents, { metricsCollector: sharedCollector })
|
|
346
|
+
|
|
347
|
+
// Both balancers route to the same agents
|
|
348
|
+
balancer1.route(createTask('t1')) // -> agent-1
|
|
349
|
+
balancer1.route(createTask('t2')) // -> agent-2
|
|
350
|
+
balancer2.route(createTask('t3')) // -> agent-1
|
|
351
|
+
balancer2.route(createTask('t4')) // -> agent-2
|
|
352
|
+
|
|
353
|
+
const metrics = sharedCollector.collect()
|
|
354
|
+
|
|
355
|
+
// Per-agent metrics should aggregate from both balancers
|
|
356
|
+
expect(metrics.perAgent['agent-1']?.routedCount).toBe(2)
|
|
357
|
+
expect(metrics.perAgent['agent-2']?.routedCount).toBe(2)
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
it('resetting shared collector should affect all balancers using it', () => {
|
|
361
|
+
const sharedCollector = createMetricsCollector()
|
|
362
|
+
const agents = [createAgent('agent-1')]
|
|
363
|
+
|
|
364
|
+
const balancer1 = createRoundRobinBalancer(agents, { metricsCollector: sharedCollector })
|
|
365
|
+
const balancer2 = createRoundRobinBalancer(agents, { metricsCollector: sharedCollector })
|
|
366
|
+
|
|
367
|
+
balancer1.route(createTask('t1'))
|
|
368
|
+
balancer2.route(createTask('t2'))
|
|
369
|
+
|
|
370
|
+
expect(sharedCollector.collect().totalRouted).toBe(2)
|
|
371
|
+
|
|
372
|
+
// Reset the shared collector
|
|
373
|
+
sharedCollector.reset()
|
|
374
|
+
|
|
375
|
+
// Both balancers' metrics should be cleared
|
|
376
|
+
expect(sharedCollector.collect().totalRouted).toBe(0)
|
|
377
|
+
|
|
378
|
+
// New routings from either balancer should work correctly
|
|
379
|
+
balancer1.route(createTask('t3'))
|
|
380
|
+
expect(sharedCollector.collect().totalRouted).toBe(1)
|
|
381
|
+
})
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
describe('MetricsCollector interface', () => {
|
|
385
|
+
it('should have collect() method returning RoutingMetrics', () => {
|
|
386
|
+
const collector = createMetricsCollector()
|
|
387
|
+
|
|
388
|
+
const metrics = collector.collect()
|
|
389
|
+
|
|
390
|
+
expect(metrics).toHaveProperty('totalRouted')
|
|
391
|
+
expect(metrics).toHaveProperty('failedRoutes')
|
|
392
|
+
expect(metrics).toHaveProperty('averageLatencyMs')
|
|
393
|
+
expect(metrics).toHaveProperty('perAgent')
|
|
394
|
+
expect(metrics).toHaveProperty('strategyUsage')
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
it('should have reset() method', () => {
|
|
398
|
+
const collector = createMetricsCollector()
|
|
399
|
+
|
|
400
|
+
expect(typeof collector.reset).toBe('function')
|
|
401
|
+
|
|
402
|
+
// Should not throw
|
|
403
|
+
collector.reset()
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
it('collect() should return a copy, not the internal state', () => {
|
|
407
|
+
const collector = createMetricsCollector()
|
|
408
|
+
const agents = [createAgent('agent-1')]
|
|
409
|
+
const balancer = createRoundRobinBalancer(agents, { metricsCollector: collector })
|
|
410
|
+
|
|
411
|
+
balancer.route(createTask('t1'))
|
|
412
|
+
|
|
413
|
+
const metrics1 = collector.collect()
|
|
414
|
+
const metrics2 = collector.collect()
|
|
415
|
+
|
|
416
|
+
// Should be equal values but different objects
|
|
417
|
+
expect(metrics1).toEqual(metrics2)
|
|
418
|
+
expect(metrics1).not.toBe(metrics2)
|
|
419
|
+
|
|
420
|
+
// Mutating the returned object should not affect internal state
|
|
421
|
+
metrics1.totalRouted = 999
|
|
422
|
+
expect(collector.collect().totalRouted).toBe(1)
|
|
423
|
+
})
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
describe('backward compatibility', () => {
|
|
427
|
+
it('collectRoutingMetrics() should still work (using default collector)', () => {
|
|
428
|
+
resetRoutingMetrics()
|
|
429
|
+
|
|
430
|
+
const agents = [createAgent('agent-1')]
|
|
431
|
+
const balancer = createRoundRobinBalancer(agents)
|
|
432
|
+
|
|
433
|
+
balancer.route(createTask('t1'))
|
|
434
|
+
|
|
435
|
+
const metrics = collectRoutingMetrics()
|
|
436
|
+
expect(metrics.totalRouted).toBe(1)
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
it('resetRoutingMetrics() should still work (resetting default collector)', () => {
|
|
440
|
+
const agents = [createAgent('agent-1')]
|
|
441
|
+
const balancer = createRoundRobinBalancer(agents)
|
|
442
|
+
|
|
443
|
+
balancer.route(createTask('t1'))
|
|
444
|
+
resetRoutingMetrics()
|
|
445
|
+
|
|
446
|
+
const metrics = collectRoutingMetrics()
|
|
447
|
+
expect(metrics.totalRouted).toBe(0)
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
it('existing balancer factory functions should work without options', () => {
|
|
451
|
+
// These should all work without providing metricsCollector
|
|
452
|
+
const agents = [createAgent('agent-1', ['code'])]
|
|
453
|
+
|
|
454
|
+
const rr = createRoundRobinBalancer(agents)
|
|
455
|
+
const lb = createLeastBusyBalancer(agents)
|
|
456
|
+
const cap = createCapabilityRouter(agents)
|
|
457
|
+
|
|
458
|
+
// All should route successfully
|
|
459
|
+
expect(rr.route(createTask('t1')).agent).not.toBeNull()
|
|
460
|
+
expect(lb.route(createTask('t2')).agent).not.toBeNull()
|
|
461
|
+
expect(cap.route(createTask('t3', ['code'])).agent).not.toBeNull()
|
|
462
|
+
})
|
|
463
|
+
})
|
|
464
|
+
})
|