@claude-flow/swarm 3.0.0-alpha.7 → 3.0.0-alpha.9

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 (130) hide show
  1. package/dist/agent-pool.d.ts.map +1 -1
  2. package/dist/agent-pool.js +10 -2
  3. package/dist/agent-pool.js.map +1 -1
  4. package/dist/attention-coordinator.d.ts +5 -4
  5. package/dist/attention-coordinator.d.ts.map +1 -1
  6. package/dist/attention-coordinator.js +17 -10
  7. package/dist/attention-coordinator.js.map +1 -1
  8. package/dist/consensus/byzantine.d.ts +2 -1
  9. package/dist/consensus/byzantine.d.ts.map +1 -1
  10. package/dist/consensus/byzantine.js +34 -8
  11. package/dist/consensus/byzantine.js.map +1 -1
  12. package/dist/consensus/gossip.d.ts +2 -1
  13. package/dist/consensus/gossip.d.ts.map +1 -1
  14. package/dist/consensus/gossip.js +27 -8
  15. package/dist/consensus/gossip.js.map +1 -1
  16. package/dist/consensus/index.d.ts +1 -1
  17. package/dist/consensus/index.d.ts.map +1 -1
  18. package/dist/consensus/index.js +2 -2
  19. package/dist/consensus/index.js.map +1 -1
  20. package/dist/consensus/raft.d.ts +2 -1
  21. package/dist/consensus/raft.d.ts.map +1 -1
  22. package/dist/consensus/raft.js +25 -9
  23. package/dist/consensus/raft.js.map +1 -1
  24. package/dist/coordination/agent-registry.d.ts +2 -2
  25. package/dist/coordination/agent-registry.d.ts.map +1 -1
  26. package/dist/coordination/agent-registry.js +1 -1
  27. package/dist/coordination/agent-registry.js.map +1 -1
  28. package/dist/coordination/swarm-hub.d.ts +5 -5
  29. package/dist/coordination/swarm-hub.d.ts.map +1 -1
  30. package/dist/coordination/swarm-hub.js +5 -5
  31. package/dist/coordination/swarm-hub.js.map +1 -1
  32. package/dist/coordination/task-orchestrator.d.ts +3 -3
  33. package/dist/coordination/task-orchestrator.d.ts.map +1 -1
  34. package/dist/coordination/task-orchestrator.js +1 -1
  35. package/dist/coordination/task-orchestrator.js.map +1 -1
  36. package/dist/index.d.ts +2 -0
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +4 -0
  39. package/dist/index.js.map +1 -1
  40. package/dist/message-bus.d.ts.map +1 -1
  41. package/dist/message-bus.js +13 -4
  42. package/dist/message-bus.js.map +1 -1
  43. package/dist/queen-coordinator.d.ts +1 -1
  44. package/dist/queen-coordinator.d.ts.map +1 -1
  45. package/dist/queen-coordinator.js +10 -3
  46. package/dist/queen-coordinator.js.map +1 -1
  47. package/dist/topology-manager.d.ts.map +1 -1
  48. package/dist/topology-manager.js +26 -1
  49. package/dist/topology-manager.js.map +1 -1
  50. package/dist/types.d.ts +3 -2
  51. package/dist/types.d.ts.map +1 -1
  52. package/dist/types.js.map +1 -1
  53. package/dist/unified-coordinator.d.ts +1 -1
  54. package/dist/unified-coordinator.d.ts.map +1 -1
  55. package/dist/unified-coordinator.js +85 -29
  56. package/dist/unified-coordinator.js.map +1 -1
  57. package/package.json +7 -2
  58. package/.claude-flow/data/pending-insights.jsonl +0 -12
  59. package/__tests__/byzantine-transport.test.ts +0 -147
  60. package/__tests__/consensus-failure-injection.test.ts +0 -161
  61. package/__tests__/consensus-transport.test.ts +0 -200
  62. package/__tests__/consensus.test.ts +0 -577
  63. package/__tests__/coordinator.test.ts +0 -501
  64. package/__tests__/federation-transport.test.ts +0 -145
  65. package/__tests__/gossip-transport.test.ts +0 -81
  66. package/__tests__/queen-coordinator.test.ts +0 -1335
  67. package/__tests__/raft-transport.test.ts +0 -121
  68. package/__tests__/topology.test.ts +0 -621
  69. package/ruvector.db +0 -0
  70. package/src/agent-pool.ts +0 -476
  71. package/src/application/commands/create-task.command.ts +0 -124
  72. package/src/application/commands/spawn-agent.command.ts +0 -122
  73. package/src/application/index.ts +0 -30
  74. package/src/application/services/swarm-application-service.ts +0 -200
  75. package/src/attention-coordinator.ts +0 -1000
  76. package/src/consensus/byzantine.ts +0 -514
  77. package/src/consensus/federation-transport.ts +0 -185
  78. package/src/consensus/gossip.ts +0 -599
  79. package/src/consensus/index.ts +0 -296
  80. package/src/consensus/raft.ts +0 -561
  81. package/src/consensus/transport.ts +0 -284
  82. package/src/coordination/agent-registry.ts +0 -544
  83. package/src/coordination/index.ts +0 -23
  84. package/src/coordination/swarm-hub.ts +0 -776
  85. package/src/coordination/task-orchestrator.ts +0 -605
  86. package/src/domain/entities/agent.d.ts +0 -151
  87. package/src/domain/entities/agent.d.ts.map +0 -1
  88. package/src/domain/entities/agent.js +0 -280
  89. package/src/domain/entities/agent.js.map +0 -1
  90. package/src/domain/entities/agent.ts +0 -370
  91. package/src/domain/entities/task.d.ts +0 -133
  92. package/src/domain/entities/task.d.ts.map +0 -1
  93. package/src/domain/entities/task.js +0 -261
  94. package/src/domain/entities/task.js.map +0 -1
  95. package/src/domain/entities/task.ts +0 -319
  96. package/src/domain/index.ts +0 -41
  97. package/src/domain/repositories/agent-repository.interface.d.ts +0 -57
  98. package/src/domain/repositories/agent-repository.interface.d.ts.map +0 -1
  99. package/src/domain/repositories/agent-repository.interface.js +0 -9
  100. package/src/domain/repositories/agent-repository.interface.js.map +0 -1
  101. package/src/domain/repositories/agent-repository.interface.ts +0 -69
  102. package/src/domain/repositories/task-repository.interface.d.ts +0 -61
  103. package/src/domain/repositories/task-repository.interface.d.ts.map +0 -1
  104. package/src/domain/repositories/task-repository.interface.js +0 -9
  105. package/src/domain/repositories/task-repository.interface.js.map +0 -1
  106. package/src/domain/repositories/task-repository.interface.ts +0 -75
  107. package/src/domain/services/coordination-service.ts +0 -320
  108. package/src/federation-hub.d.ts +0 -284
  109. package/src/federation-hub.d.ts.map +0 -1
  110. package/src/federation-hub.js +0 -692
  111. package/src/federation-hub.js.map +0 -1
  112. package/src/federation-hub.ts +0 -979
  113. package/src/index.ts +0 -348
  114. package/src/message-bus.ts +0 -607
  115. package/src/queen-coordinator.ts +0 -2025
  116. package/src/shared/events.ts +0 -285
  117. package/src/shared/types.ts +0 -389
  118. package/src/topology-manager.ts +0 -656
  119. package/src/types.ts +0 -545
  120. package/src/unified-coordinator.ts +0 -1844
  121. package/src/workers/index.ts +0 -65
  122. package/src/workers/worker-dispatch.d.ts +0 -234
  123. package/src/workers/worker-dispatch.d.ts.map +0 -1
  124. package/src/workers/worker-dispatch.js +0 -842
  125. package/src/workers/worker-dispatch.js.map +0 -1
  126. package/src/workers/worker-dispatch.ts +0 -1076
  127. package/tmp.json +0 -0
  128. package/tsconfig.json +0 -9
  129. package/tsconfig.tsbuildinfo +0 -1
  130. package/vitest.config.ts +0 -20
@@ -1,501 +0,0 @@
1
- /**
2
- * UnifiedSwarmCoordinator Tests
3
- * Comprehensive tests for the unified swarm coordination system
4
- */
5
-
6
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
7
- import { UnifiedSwarmCoordinator, createUnifiedSwarmCoordinator } from '../src/unified-coordinator.js';
8
- import type {
9
- AgentState,
10
- TaskDefinition,
11
- CoordinatorConfig,
12
- AgentType,
13
- TaskType,
14
- TaskPriority,
15
- } from '../src/types.js';
16
-
17
- describe('UnifiedSwarmCoordinator', () => {
18
- let coordinator: UnifiedSwarmCoordinator;
19
-
20
- beforeEach(async () => {
21
- coordinator = createUnifiedSwarmCoordinator({
22
- maxAgents: 20,
23
- maxTasks: 100,
24
- heartbeatIntervalMs: 1000,
25
- healthCheckIntervalMs: 2000,
26
- taskTimeoutMs: 10000,
27
- topology: {
28
- type: 'hierarchical',
29
- maxAgents: 20,
30
- },
31
- consensus: {
32
- algorithm: 'raft',
33
- threshold: 0.66,
34
- timeoutMs: 5000,
35
- maxRounds: 5,
36
- requireQuorum: true,
37
- },
38
- });
39
-
40
- await coordinator.initialize();
41
- });
42
-
43
- afterEach(async () => {
44
- await coordinator.shutdown();
45
- });
46
-
47
- describe('Initialization', () => {
48
- it('should initialize successfully', () => {
49
- const state = coordinator.getState();
50
- expect(state.status).toBe('running');
51
- expect(state.id).toBeDefined();
52
- expect(state.id.id).toContain('swarm_');
53
- });
54
-
55
- it('should create default configuration', () => {
56
- const state = coordinator.getState();
57
- expect(state.topology.type).toBe('hierarchical');
58
- expect(state.agents.size).toBe(0);
59
- expect(state.tasks.size).toBe(0);
60
- });
61
-
62
- it('should initialize with mesh topology', async () => {
63
- const meshCoordinator = createUnifiedSwarmCoordinator({
64
- topology: { type: 'mesh', maxAgents: 10 },
65
- });
66
- await meshCoordinator.initialize();
67
-
68
- expect(meshCoordinator.getTopology()).toBe('mesh');
69
-
70
- await meshCoordinator.shutdown();
71
- });
72
- });
73
-
74
- describe('Agent Registration', () => {
75
- it('should register a new agent', async () => {
76
- const agentData: Omit<AgentState, 'id'> = {
77
- name: 'test-agent-1',
78
- type: 'coder',
79
- status: 'idle',
80
- capabilities: createTestCapabilities(),
81
- metrics: createTestMetrics(),
82
- workload: 0,
83
- health: 1.0,
84
- lastHeartbeat: new Date(),
85
- topologyRole: 'worker',
86
- connections: [],
87
- };
88
-
89
- const agentId = await coordinator.registerAgent(agentData);
90
-
91
- expect(agentId).toBeDefined();
92
- expect(agentId).toContain('agent_');
93
-
94
- const agent = coordinator.getAgent(agentId);
95
- expect(agent).toBeDefined();
96
- expect(agent?.name).toBe('test-agent-1');
97
- expect(agent?.type).toBe('coder');
98
- });
99
-
100
- it('should register multiple agents', async () => {
101
- const agentTypes: AgentType[] = ['coder', 'tester', 'reviewer'];
102
- const agentIds: string[] = [];
103
-
104
- for (const type of agentTypes) {
105
- const agentData: Omit<AgentState, 'id'> = {
106
- name: `agent-${type}`,
107
- type,
108
- status: 'idle',
109
- capabilities: createTestCapabilities(),
110
- metrics: createTestMetrics(),
111
- workload: 0,
112
- health: 1.0,
113
- lastHeartbeat: new Date(),
114
- topologyRole: 'worker',
115
- connections: [],
116
- };
117
-
118
- const id = await coordinator.registerAgent(agentData);
119
- agentIds.push(id);
120
- }
121
-
122
- expect(agentIds).toHaveLength(3);
123
- expect(coordinator.getAllAgents()).toHaveLength(3);
124
- });
125
-
126
- it('should throw error when max agents reached', async () => {
127
- const smallCoordinator = createUnifiedSwarmCoordinator({
128
- maxAgents: 2,
129
- });
130
- await smallCoordinator.initialize();
131
-
132
- // Register 2 agents
133
- for (let i = 0; i < 2; i++) {
134
- await smallCoordinator.registerAgent({
135
- name: `agent-${i}`,
136
- type: 'worker',
137
- status: 'idle',
138
- capabilities: createTestCapabilities(),
139
- metrics: createTestMetrics(),
140
- workload: 0,
141
- health: 1.0,
142
- lastHeartbeat: new Date(),
143
- topologyRole: 'worker',
144
- connections: [],
145
- });
146
- }
147
-
148
- // Third should fail
149
- await expect(
150
- smallCoordinator.registerAgent({
151
- name: 'agent-overflow',
152
- type: 'worker',
153
- status: 'idle',
154
- capabilities: createTestCapabilities(),
155
- metrics: createTestMetrics(),
156
- workload: 0,
157
- health: 1.0,
158
- lastHeartbeat: new Date(),
159
- topologyRole: 'worker',
160
- connections: [],
161
- })
162
- ).rejects.toThrow('Maximum agents');
163
-
164
- await smallCoordinator.shutdown();
165
- });
166
-
167
- it('should unregister an agent', async () => {
168
- const agentId = await coordinator.registerAgent({
169
- name: 'temporary-agent',
170
- type: 'worker',
171
- status: 'idle',
172
- capabilities: createTestCapabilities(),
173
- metrics: createTestMetrics(),
174
- workload: 0,
175
- health: 1.0,
176
- lastHeartbeat: new Date(),
177
- topologyRole: 'worker',
178
- connections: [],
179
- });
180
-
181
- expect(coordinator.getAgent(agentId)).toBeDefined();
182
-
183
- await coordinator.unregisterAgent(agentId);
184
-
185
- expect(coordinator.getAgent(agentId)).toBeUndefined();
186
- });
187
- });
188
-
189
- describe('Task Management', () => {
190
- let agentId: string;
191
-
192
- beforeEach(async () => {
193
- agentId = await coordinator.registerAgent({
194
- name: 'worker-agent',
195
- type: 'coder',
196
- status: 'idle',
197
- capabilities: createTestCapabilities(),
198
- metrics: createTestMetrics(),
199
- workload: 0,
200
- health: 1.0,
201
- lastHeartbeat: new Date(),
202
- topologyRole: 'worker',
203
- connections: [],
204
- });
205
- });
206
-
207
- it('should submit a new task', async () => {
208
- const taskData: Omit<TaskDefinition, 'id' | 'status' | 'createdAt'> = {
209
- type: 'coding',
210
- name: 'Test Task',
211
- description: 'A test coding task',
212
- priority: 'normal',
213
- dependencies: [],
214
- input: { code: 'console.log("test")' },
215
- timeoutMs: 5000,
216
- retries: 0,
217
- maxRetries: 3,
218
- metadata: {},
219
- };
220
-
221
- const taskId = await coordinator.submitTask(taskData);
222
-
223
- expect(taskId).toBeDefined();
224
- expect(taskId).toContain('task_');
225
-
226
- const task = coordinator.getTask(taskId);
227
- expect(task).toBeDefined();
228
- expect(task?.name).toBe('Test Task');
229
- expect(task?.status).toBe('assigned');
230
- });
231
-
232
- it('should assign task to available agent', async () => {
233
- const taskId = await coordinator.submitTask({
234
- type: 'coding',
235
- name: 'Code Task',
236
- description: 'Task for coder',
237
- priority: 'high',
238
- dependencies: [],
239
- input: {},
240
- timeoutMs: 5000,
241
- retries: 0,
242
- maxRetries: 3,
243
- metadata: {},
244
- });
245
-
246
- const task = coordinator.getTask(taskId);
247
- expect(task?.assignedTo).toBeDefined();
248
- expect(task?.assignedTo?.id).toBe(agentId);
249
-
250
- const agent = coordinator.getAgent(agentId);
251
- expect(agent?.status).toBe('busy');
252
- expect(agent?.currentTask?.id).toBe(taskId);
253
- });
254
-
255
- it('should cancel a task', async () => {
256
- const taskId = await coordinator.submitTask({
257
- type: 'testing',
258
- name: 'Test Task',
259
- description: 'Task to cancel',
260
- priority: 'normal',
261
- dependencies: [],
262
- input: {},
263
- timeoutMs: 5000,
264
- retries: 0,
265
- maxRetries: 3,
266
- metadata: {},
267
- });
268
-
269
- await coordinator.cancelTask(taskId);
270
-
271
- const task = coordinator.getTask(taskId);
272
- expect(task?.status).toBe('cancelled');
273
-
274
- const agent = coordinator.getAgent(agentId);
275
- expect(agent?.status).toBe('idle');
276
- expect(agent?.currentTask).toBeUndefined();
277
- });
278
-
279
- it('should get tasks by status', async () => {
280
- await coordinator.submitTask({
281
- type: 'coding',
282
- name: 'Task 1',
283
- description: 'First task',
284
- priority: 'normal',
285
- dependencies: [],
286
- input: {},
287
- timeoutMs: 5000,
288
- retries: 0,
289
- maxRetries: 3,
290
- metadata: {},
291
- });
292
-
293
- const assignedTasks = coordinator.getTasksByStatus('assigned');
294
- expect(assignedTasks).toHaveLength(1);
295
- });
296
- });
297
-
298
- describe('Topology Management', () => {
299
- it('should maintain hierarchical topology', async () => {
300
- // Register queen
301
- const queenId = await coordinator.registerAgent({
302
- name: 'queen-agent',
303
- type: 'queen',
304
- status: 'idle',
305
- capabilities: createTestCapabilities(),
306
- metrics: createTestMetrics(),
307
- workload: 0,
308
- health: 1.0,
309
- lastHeartbeat: new Date(),
310
- topologyRole: 'queen',
311
- connections: [],
312
- });
313
-
314
- // Register workers
315
- const worker1Id = await coordinator.registerAgent({
316
- name: 'worker-1',
317
- type: 'worker',
318
- status: 'idle',
319
- capabilities: createTestCapabilities(),
320
- metrics: createTestMetrics(),
321
- workload: 0,
322
- health: 1.0,
323
- lastHeartbeat: new Date(),
324
- topologyRole: 'worker',
325
- connections: [],
326
- });
327
-
328
- const state = coordinator.getState();
329
- expect(state.topology.nodes).toHaveLength(2);
330
-
331
- const queen = state.topology.nodes.find(n => n.role === 'queen');
332
- expect(queen).toBeDefined();
333
- expect(queen?.agentId).toBe(queenId);
334
- });
335
-
336
- it('should change topology type', () => {
337
- coordinator.setTopology('mesh');
338
- expect(coordinator.getTopology()).toBe('mesh');
339
-
340
- coordinator.setTopology('centralized');
341
- expect(coordinator.getTopology()).toBe('centralized');
342
- });
343
- });
344
-
345
- describe('Lifecycle Management', () => {
346
- it('should pause and resume coordinator', async () => {
347
- await coordinator.pause();
348
- expect(coordinator.getState().status).toBe('paused');
349
-
350
- await coordinator.resume();
351
- expect(coordinator.getState().status).toBe('running');
352
- });
353
-
354
- it('should shutdown cleanly', async () => {
355
- const agentId = await coordinator.registerAgent({
356
- name: 'agent-to-cleanup',
357
- type: 'worker',
358
- status: 'idle',
359
- capabilities: createTestCapabilities(),
360
- metrics: createTestMetrics(),
361
- workload: 0,
362
- health: 1.0,
363
- lastHeartbeat: new Date(),
364
- topologyRole: 'worker',
365
- connections: [],
366
- });
367
-
368
- await coordinator.shutdown();
369
-
370
- expect(coordinator.getState().status).toBe('stopped');
371
- expect(coordinator.getAllAgents()).toHaveLength(0);
372
- });
373
- });
374
-
375
- describe('Domain-Based Routing', () => {
376
- it('should assign tasks to specific domains', async () => {
377
- // Register agent with domain
378
- const result = await coordinator.registerAgentWithDomain(
379
- {
380
- name: 'security-agent',
381
- type: 'specialist',
382
- status: 'idle',
383
- capabilities: createTestCapabilities(),
384
- metrics: createTestMetrics(),
385
- workload: 0,
386
- health: 1.0,
387
- lastHeartbeat: new Date(),
388
- topologyRole: 'worker',
389
- connections: [],
390
- },
391
- 2 // Security domain (agent #2)
392
- );
393
-
394
- expect(result.domain).toBe('security');
395
- expect(result.agentId).toBeDefined();
396
- });
397
-
398
- it('should get agent domain from agent number', () => {
399
- expect(coordinator.getAgentDomain(1)).toBe('queen');
400
- expect(coordinator.getAgentDomain(2)).toBe('security');
401
- expect(coordinator.getAgentDomain(5)).toBe('core');
402
- expect(coordinator.getAgentDomain(10)).toBe('integration');
403
- expect(coordinator.getAgentDomain(13)).toBe('support');
404
- });
405
-
406
- it('should spawn full 15-agent hierarchy', async () => {
407
- const hierarchy = await coordinator.spawnFullHierarchy();
408
-
409
- expect(hierarchy.size).toBe(15);
410
- expect(hierarchy.get(1)?.domain).toBe('queen');
411
- expect(hierarchy.get(2)?.domain).toBe('security');
412
- expect(hierarchy.get(5)?.domain).toBe('core');
413
-
414
- const status = coordinator.getStatus();
415
- expect(status.domains).toHaveLength(5);
416
- });
417
- });
418
-
419
- describe('Performance Metrics', () => {
420
- it('should track coordinator metrics', async () => {
421
- const metrics = coordinator.getMetrics();
422
-
423
- expect(metrics).toBeDefined();
424
- expect(metrics.uptime).toBeGreaterThanOrEqual(0);
425
- expect(metrics.activeAgents).toBe(0);
426
- expect(metrics.totalTasks).toBe(0);
427
- expect(metrics.completedTasks).toBe(0);
428
- });
429
-
430
- it('should generate performance report', async () => {
431
- const report = coordinator.getPerformanceReport();
432
-
433
- expect(report).toBeDefined();
434
- expect(report.timestamp).toBeInstanceOf(Date);
435
- expect(report.coordinationLatencyP50).toBeGreaterThanOrEqual(0);
436
- expect(report.messagesPerSecond).toBeGreaterThanOrEqual(0);
437
- });
438
-
439
- it('should report healthy status', async () => {
440
- // Add at least one agent to be considered healthy
441
- await coordinator.registerAgent({
442
- name: 'health-agent',
443
- type: 'worker',
444
- status: 'idle',
445
- capabilities: createTestCapabilities(),
446
- metrics: createTestMetrics(),
447
- workload: 0,
448
- health: 1.0,
449
- lastHeartbeat: new Date(),
450
- topologyRole: 'worker',
451
- connections: [],
452
- });
453
-
454
- // Wait for health check interval (2000ms configured in beforeEach)
455
- await new Promise(resolve => setTimeout(resolve, 2500));
456
-
457
- const metrics = coordinator.getMetrics();
458
- expect(metrics.activeAgents).toBeGreaterThan(0);
459
- expect(coordinator.isHealthy()).toBe(true);
460
- });
461
- });
462
- });
463
-
464
- // ===== TEST UTILITIES =====
465
-
466
- function createTestCapabilities() {
467
- return {
468
- codeGeneration: true,
469
- codeReview: true,
470
- testing: true,
471
- documentation: true,
472
- research: true,
473
- analysis: true,
474
- coordination: false,
475
- languages: ['typescript', 'javascript'],
476
- frameworks: ['node', 'vitest'],
477
- domains: ['backend', 'testing'],
478
- tools: ['git', 'npm'],
479
- maxConcurrentTasks: 3,
480
- maxMemoryUsage: 512 * 1024 * 1024,
481
- maxExecutionTime: 300000,
482
- reliability: 0.95,
483
- speed: 1.0,
484
- quality: 0.9,
485
- };
486
- }
487
-
488
- function createTestMetrics() {
489
- return {
490
- tasksCompleted: 0,
491
- tasksFailed: 0,
492
- averageExecutionTime: 0,
493
- successRate: 1.0,
494
- cpuUsage: 0,
495
- memoryUsage: 0,
496
- messagesProcessed: 0,
497
- lastActivity: new Date(),
498
- responseTime: 0,
499
- health: 1.0,
500
- };
501
- }
@@ -1,145 +0,0 @@
1
- /**
2
- * ADR-095 G2 — FederationTransport tests.
3
- *
4
- * Uses a mock AgenticFlowTransportLike: a tiny in-memory mesh where
5
- * `send(address, msg)` delivers to the node registered at that address.
6
- * That's enough to exercise the request-response correlation, broadcast,
7
- * timeout, and Ed25519-signing paths without a real WS.
8
- */
9
- import { describe, it, expect } from 'vitest';
10
- import {
11
- FederationTransport,
12
- type AgenticFlowTransportLike,
13
- } from '../src/consensus/federation-transport.js';
14
- import { generateNodeKeyPair, type ConsensusMessage } from '../src/consensus/transport.js';
15
-
16
- /** A mock mesh of agentic-flow-like transports keyed by address. */
17
- class MockMesh {
18
- private readonly nodes = new Map<string, MockWire>();
19
- register(address: string, w: MockWire) { this.nodes.set(address, w); }
20
- deliver(address: string, msg: { from?: string; type?: string; payload: unknown }) {
21
- const target = this.nodes.get(address);
22
- if (target) queueMicrotask(() => target.receive(msg));
23
- }
24
- }
25
-
26
- class MockWire implements AgenticFlowTransportLike {
27
- private handler: ((m: { from?: string; type?: string; payload: unknown }) => void | Promise<void>) | null = null;
28
- constructor(private readonly mesh: MockMesh, address: string) {
29
- mesh.register(address, this);
30
- }
31
- async send(address: string, message: { type?: string; payload: unknown; streamId?: string }): Promise<void> {
32
- // Stamp `from` from the embedded msg.from so the mesh can route replies.
33
- const env = message.payload as { msg?: { from?: string } };
34
- this.mesh.deliver(address, { from: env?.msg?.from, type: message.type, payload: message.payload });
35
- }
36
- onMessage(handler: (m: { from?: string; type?: string; payload: unknown }) => void | Promise<void>): void {
37
- this.handler = handler;
38
- }
39
- receive(m: { from?: string; type?: string; payload: unknown }) {
40
- void this.handler?.(m);
41
- }
42
- close(): void { /* noop */ }
43
- }
44
-
45
- function build(ids: string[]) {
46
- const mesh = new MockMesh();
47
- const addr: Record<string, string> = {};
48
- ids.forEach((id) => { addr[id] = `ws://${id}:9101`; });
49
- const addressOf = (id: string) => addr[id];
50
- const transports = new Map<string, FederationTransport>();
51
- for (const id of ids) {
52
- const wire = new MockWire(mesh, addr[id]);
53
- transports.set(id, new FederationTransport(wire, { nodeId: id, addressOf, peerIds: () => ids }));
54
- }
55
- return { transports, addressOf };
56
- }
57
-
58
- describe('ADR-095 G2 — FederationTransport', () => {
59
- it('send → peer handler → reply round-trips via correlation id', async () => {
60
- const { transports } = build(['a', 'b']);
61
- transports.get('b')!.onMessage(async (msg) => {
62
- expect(msg.from).toBe('a');
63
- return { type: 'vote-response', from: 'b', to: 'a', payload: { granted: true, term: msg.term } };
64
- });
65
- const reply = await transports.get('a')!.send('b', { type: 'request-vote', payload: { candidateId: 'a' }, term: 1 });
66
- expect(reply).not.toBeNull();
67
- expect(reply!.type).toBe('vote-response');
68
- expect((reply!.payload as { granted: boolean }).granted).toBe(true);
69
- await transports.get('a')!.close(); await transports.get('b')!.close();
70
- });
71
-
72
- it('broadcast reaches every peer (no replies expected)', async () => {
73
- const { transports } = build(['a', 'b', 'c']);
74
- const seenB: string[] = []; const seenC: string[] = [];
75
- transports.get('b')!.onMessage((m) => { seenB.push(m.type); });
76
- transports.get('c')!.onMessage((m) => { seenC.push(m.type); });
77
- await transports.get('a')!.broadcast({ type: 'pre-prepare', payload: { digest: 'd1' }, viewNumber: 0 });
78
- await new Promise(r => setTimeout(r, 10)); // let microtasks drain
79
- expect(seenB).toEqual(['pre-prepare']);
80
- expect(seenC).toEqual(['pre-prepare']);
81
- for (const t of transports.values()) await t.close();
82
- });
83
-
84
- it('send to a peer with no address rejects', async () => {
85
- const { transports } = build(['a']);
86
- await expect(transports.get('a')!.send('ghost', { type: 'x', payload: {} })).rejects.toThrow(/no address/);
87
- await transports.get('a')!.close();
88
- });
89
-
90
- it('send times out if no reply arrives', async () => {
91
- const { transports } = build(['a', 'b']);
92
- transports.get('b')!.onMessage(() => { /* no reply */ });
93
- await expect(transports.get('a')!.send('b', { type: 'x', payload: {} }, 50)).rejects.toThrow(/timed out/);
94
- await transports.get('a')!.close(); await transports.get('b')!.close();
95
- });
96
-
97
- it('peers() excludes self', async () => {
98
- const { transports } = build(['a', 'b', 'c']);
99
- expect(new Set(transports.get('a')!.peers())).toEqual(new Set(['b', 'c']));
100
- for (const t of transports.values()) await t.close();
101
- });
102
-
103
- it('signs outbound + verifies inbound when keypairs are configured', async () => {
104
- const mesh = new MockMesh();
105
- const addr: Record<string, string> = { a: 'ws://a:9101', b: 'ws://b:9101' };
106
- const addressOf = (id: string) => addr[id];
107
- const kpA = generateNodeKeyPair(); const kpB = generateNodeKeyPair();
108
- const pubs: Record<string, string> = { a: kpA.publicKeyPem, b: kpB.publicKeyPem };
109
- const resolvePeerPublicKey = (id: string) => pubs[id];
110
-
111
- const wireA = new MockWire(mesh, addr.a);
112
- const wireB = new MockWire(mesh, addr.b);
113
- const a = new FederationTransport(wireA, { nodeId: 'a', addressOf, peerIds: () => ['a', 'b'], keyPair: kpA, resolvePeerPublicKey });
114
- const b = new FederationTransport(wireB, { nodeId: 'b', addressOf, peerIds: () => ['a', 'b'], keyPair: kpB, resolvePeerPublicKey });
115
-
116
- let received: ConsensusMessage | null = null;
117
- b.onMessage((m) => { received = m; return { type: 'ack', from: 'b', payload: {} }; });
118
- const reply = await a.send('b', { type: 'commit', payload: { digest: 'x' } });
119
- expect(reply).not.toBeNull();
120
- expect(received).not.toBeNull();
121
- expect(received!.signature).toBeTruthy();
122
- expect(received!.seq).toBe(1);
123
- await a.close(); await b.close();
124
- });
125
-
126
- it('drops an unsigned inbound message at a signing-enabled node', async () => {
127
- const mesh = new MockMesh();
128
- const addr: Record<string, string> = { a: 'ws://a:9101', b: 'ws://b:9101' };
129
- const addressOf = (id: string) => addr[id];
130
- const kpB = generateNodeKeyPair();
131
- const resolvePeerPublicKey = (id: string) => (id === 'b' ? kpB.publicKeyPem : undefined);
132
-
133
- const wireA = new MockWire(mesh, addr.a);
134
- const wireB = new MockWire(mesh, addr.b);
135
- const a = new FederationTransport(wireA, { nodeId: 'a', addressOf, peerIds: () => ['a', 'b'] }); // no keypair → unsigned
136
- const b = new FederationTransport(wireB, { nodeId: 'b', addressOf, peerIds: () => ['a', 'b'], keyPair: kpB, resolvePeerPublicKey });
137
-
138
- let handlerCalled = false;
139
- b.onMessage(() => { handlerCalled = true; return { type: 'ack', from: 'b', payload: {} }; });
140
- // The unsigned message is dropped at `b`, so `a`'s send never gets a reply → times out.
141
- await expect(a.send('b', { type: 'commit', payload: {} }, 50)).rejects.toThrow(/timed out/);
142
- expect(handlerCalled).toBe(false);
143
- await a.close(); await b.close();
144
- });
145
- });