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.
Files changed (197) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +136 -180
  3. package/dist/actions.d.ts.map +1 -1
  4. package/dist/actions.js +34 -21
  5. package/dist/actions.js.map +1 -1
  6. package/dist/agent-comms.d.ts +438 -0
  7. package/dist/agent-comms.d.ts.map +1 -0
  8. package/dist/agent-comms.js +677 -0
  9. package/dist/agent-comms.js.map +1 -0
  10. package/dist/approve.d.ts +40 -8
  11. package/dist/approve.d.ts.map +1 -1
  12. package/dist/approve.js +86 -20
  13. package/dist/approve.js.map +1 -1
  14. package/dist/ask.d.ts +38 -7
  15. package/dist/ask.d.ts.map +1 -1
  16. package/dist/ask.js +85 -25
  17. package/dist/ask.js.map +1 -1
  18. package/dist/browse.d.ts +223 -0
  19. package/dist/browse.d.ts.map +1 -0
  20. package/dist/browse.js +392 -0
  21. package/dist/browse.js.map +1 -0
  22. package/dist/capability-tiers.d.ts +230 -0
  23. package/dist/capability-tiers.d.ts.map +1 -0
  24. package/dist/capability-tiers.js +388 -0
  25. package/dist/capability-tiers.js.map +1 -0
  26. package/dist/cascade-context.d.ts +523 -0
  27. package/dist/cascade-context.d.ts.map +1 -0
  28. package/dist/cascade-context.js +494 -0
  29. package/dist/cascade-context.js.map +1 -0
  30. package/dist/client.d.ts +162 -0
  31. package/dist/client.d.ts.map +1 -0
  32. package/dist/client.js +64 -0
  33. package/dist/client.js.map +1 -0
  34. package/dist/decide.d.ts +42 -6
  35. package/dist/decide.d.ts.map +1 -1
  36. package/dist/decide.js +54 -11
  37. package/dist/decide.js.map +1 -1
  38. package/dist/do.d.ts +36 -7
  39. package/dist/do.d.ts.map +1 -1
  40. package/dist/do.js +82 -39
  41. package/dist/do.js.map +1 -1
  42. package/dist/error-escalation.d.ts +416 -0
  43. package/dist/error-escalation.d.ts.map +1 -0
  44. package/dist/error-escalation.js +656 -0
  45. package/dist/error-escalation.js.map +1 -0
  46. package/dist/generate.d.ts +48 -7
  47. package/dist/generate.d.ts.map +1 -1
  48. package/dist/generate.js +49 -8
  49. package/dist/generate.js.map +1 -1
  50. package/dist/goals.d.ts +10 -9
  51. package/dist/goals.d.ts.map +1 -1
  52. package/dist/goals.js +30 -24
  53. package/dist/goals.js.map +1 -1
  54. package/dist/image.d.ts +189 -0
  55. package/dist/image.d.ts.map +1 -0
  56. package/dist/image.js +528 -0
  57. package/dist/image.js.map +1 -0
  58. package/dist/index.d.ts +59 -2
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +92 -2
  61. package/dist/index.js.map +1 -1
  62. package/dist/is.d.ts +45 -10
  63. package/dist/is.d.ts.map +1 -1
  64. package/dist/is.js +56 -21
  65. package/dist/is.js.map +1 -1
  66. package/dist/kpis.d.ts +24 -15
  67. package/dist/kpis.d.ts.map +1 -1
  68. package/dist/kpis.js +16 -14
  69. package/dist/kpis.js.map +1 -1
  70. package/dist/load-balancing.d.ts +395 -0
  71. package/dist/load-balancing.d.ts.map +1 -0
  72. package/dist/load-balancing.js +991 -0
  73. package/dist/load-balancing.js.map +1 -0
  74. package/dist/logger.d.ts +76 -0
  75. package/dist/logger.d.ts.map +1 -0
  76. package/dist/logger.js +39 -0
  77. package/dist/logger.js.map +1 -0
  78. package/dist/notify.d.ts +38 -9
  79. package/dist/notify.d.ts.map +1 -1
  80. package/dist/notify.js +72 -17
  81. package/dist/notify.js.map +1 -1
  82. package/dist/role.d.ts +5 -4
  83. package/dist/role.d.ts.map +1 -1
  84. package/dist/role.js +13 -10
  85. package/dist/role.js.map +1 -1
  86. package/dist/runtime.d.ts +310 -0
  87. package/dist/runtime.d.ts.map +1 -0
  88. package/dist/runtime.js +510 -0
  89. package/dist/runtime.js.map +1 -0
  90. package/dist/team.d.ts +11 -6
  91. package/dist/team.d.ts.map +1 -1
  92. package/dist/team.js +22 -15
  93. package/dist/team.js.map +1 -1
  94. package/dist/transports/email.d.ts +318 -0
  95. package/dist/transports/email.d.ts.map +1 -0
  96. package/dist/transports/email.js +779 -0
  97. package/dist/transports/email.js.map +1 -0
  98. package/dist/transports/slack.d.ts +515 -0
  99. package/dist/transports/slack.d.ts.map +1 -0
  100. package/dist/transports/slack.js +844 -0
  101. package/dist/transports/slack.js.map +1 -0
  102. package/dist/transports.d.ts.map +1 -1
  103. package/dist/transports.js +44 -25
  104. package/dist/transports.js.map +1 -1
  105. package/dist/types.d.ts +149 -19
  106. package/dist/types.d.ts.map +1 -1
  107. package/dist/types.js +6 -0
  108. package/dist/types.js.map +1 -1
  109. package/dist/utils/id.d.ts +19 -0
  110. package/dist/utils/id.d.ts.map +1 -0
  111. package/dist/utils/id.js +21 -0
  112. package/dist/utils/id.js.map +1 -0
  113. package/dist/video.d.ts +203 -0
  114. package/dist/video.d.ts.map +1 -0
  115. package/dist/video.js +528 -0
  116. package/dist/video.js.map +1 -0
  117. package/dist/worker.d.ts +343 -0
  118. package/dist/worker.d.ts.map +1 -0
  119. package/dist/worker.js +698 -0
  120. package/dist/worker.js.map +1 -0
  121. package/package.json +24 -5
  122. package/src/actions.ts +48 -38
  123. package/src/agent-comms.ts +1200 -0
  124. package/src/approve.ts +91 -20
  125. package/src/ask.ts +99 -25
  126. package/src/browse.ts +627 -0
  127. package/src/capability-tiers.ts +545 -0
  128. package/src/cascade-context.ts +648 -0
  129. package/src/client.ts +221 -0
  130. package/src/decide.ts +81 -35
  131. package/src/do.ts +98 -52
  132. package/src/error-escalation.ts +1123 -0
  133. package/src/generate.ts +52 -18
  134. package/src/goals.ts +36 -27
  135. package/src/image.ts +816 -0
  136. package/src/index.ts +410 -2
  137. package/src/is.ts +59 -25
  138. package/src/kpis.ts +41 -36
  139. package/src/load-balancing.ts +1467 -0
  140. package/src/logger.ts +93 -0
  141. package/src/notify.ts +78 -17
  142. package/src/role.ts +30 -20
  143. package/src/runtime.ts +796 -0
  144. package/src/team.ts +24 -19
  145. package/src/transports/email.ts +1160 -0
  146. package/src/transports/slack.ts +1320 -0
  147. package/src/transports.ts +58 -43
  148. package/src/types.ts +182 -46
  149. package/src/utils/id.ts +21 -0
  150. package/src/video.ts +906 -0
  151. package/src/worker.ts +1007 -0
  152. package/test/agent-comms.test.ts +1397 -0
  153. package/test/approve.test.ts +305 -0
  154. package/test/ask.test.ts +274 -0
  155. package/test/browse.test.ts +361 -0
  156. package/test/capability-tiers.test.ts +631 -0
  157. package/test/cascade-context.test.ts +692 -0
  158. package/test/decide.test.ts +252 -0
  159. package/test/do.test.ts +144 -0
  160. package/test/error-escalation.test.ts +1205 -0
  161. package/test/error-logging.test.ts +357 -0
  162. package/test/generate.test.ts +319 -0
  163. package/test/image.test.ts +398 -0
  164. package/test/is.test.ts +287 -0
  165. package/test/load-balancing-safety.test.ts +404 -0
  166. package/test/load-balancing-thread-safety.test.ts +464 -0
  167. package/test/load-balancing.test.ts +1145 -0
  168. package/test/notify.test.ts +434 -0
  169. package/test/primitives.test.ts +320 -0
  170. package/test/runtime-integration.test.ts +892 -0
  171. package/test/transports/crypto.test.ts +230 -0
  172. package/test/transports/email.test.ts +866 -0
  173. package/test/transports/id-generation.test.ts +91 -0
  174. package/test/transports/slack.test.ts +760 -0
  175. package/test/type-safety.test.ts +834 -0
  176. package/test/types.test.ts +95 -2
  177. package/test/video.test.ts +530 -0
  178. package/test/worker.test.ts +1433 -0
  179. package/tsconfig.json +4 -1
  180. package/vitest.config.ts +42 -0
  181. package/wrangler.jsonc +36 -0
  182. package/.turbo/turbo-build.log +0 -5
  183. package/src/actions.js +0 -436
  184. package/src/approve.js +0 -234
  185. package/src/ask.js +0 -226
  186. package/src/decide.js +0 -244
  187. package/src/do.js +0 -227
  188. package/src/generate.js +0 -298
  189. package/src/goals.js +0 -205
  190. package/src/index.js +0 -68
  191. package/src/is.js +0 -317
  192. package/src/kpis.js +0 -270
  193. package/src/notify.js +0 -219
  194. package/src/role.js +0 -110
  195. package/src/team.js +0 -130
  196. package/src/transports.js +0 -357
  197. 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
+ })