@sparkleideas/swarm 3.0.0-alpha.7
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/MIGRATION.md +472 -0
- package/README.md +634 -0
- package/__tests__/consensus.test.ts +577 -0
- package/__tests__/coordinator.test.ts +501 -0
- package/__tests__/queen-coordinator.test.ts +1335 -0
- package/__tests__/topology.test.ts +621 -0
- package/package.json +32 -0
- package/src/agent-pool.ts +476 -0
- package/src/application/commands/create-task.command.ts +124 -0
- package/src/application/commands/spawn-agent.command.ts +122 -0
- package/src/application/index.ts +30 -0
- package/src/application/services/swarm-application-service.ts +200 -0
- package/src/attention-coordinator.ts +1000 -0
- package/src/consensus/byzantine.ts +431 -0
- package/src/consensus/gossip.ts +513 -0
- package/src/consensus/index.ts +267 -0
- package/src/consensus/raft.ts +443 -0
- package/src/coordination/agent-registry.ts +544 -0
- package/src/coordination/index.ts +23 -0
- package/src/coordination/swarm-hub.ts +776 -0
- package/src/coordination/task-orchestrator.ts +605 -0
- package/src/domain/entities/agent.d.ts +151 -0
- package/src/domain/entities/agent.d.ts.map +1 -0
- package/src/domain/entities/agent.js +280 -0
- package/src/domain/entities/agent.js.map +1 -0
- package/src/domain/entities/agent.ts +370 -0
- package/src/domain/entities/task.d.ts +133 -0
- package/src/domain/entities/task.d.ts.map +1 -0
- package/src/domain/entities/task.js +261 -0
- package/src/domain/entities/task.js.map +1 -0
- package/src/domain/entities/task.ts +319 -0
- package/src/domain/index.ts +41 -0
- package/src/domain/repositories/agent-repository.interface.d.ts +57 -0
- package/src/domain/repositories/agent-repository.interface.d.ts.map +1 -0
- package/src/domain/repositories/agent-repository.interface.js +9 -0
- package/src/domain/repositories/agent-repository.interface.js.map +1 -0
- package/src/domain/repositories/agent-repository.interface.ts +69 -0
- package/src/domain/repositories/task-repository.interface.d.ts +61 -0
- package/src/domain/repositories/task-repository.interface.d.ts.map +1 -0
- package/src/domain/repositories/task-repository.interface.js +9 -0
- package/src/domain/repositories/task-repository.interface.js.map +1 -0
- package/src/domain/repositories/task-repository.interface.ts +75 -0
- package/src/domain/services/coordination-service.ts +320 -0
- package/src/federation-hub.d.ts +284 -0
- package/src/federation-hub.d.ts.map +1 -0
- package/src/federation-hub.js +692 -0
- package/src/federation-hub.js.map +1 -0
- package/src/federation-hub.ts +979 -0
- package/src/index.ts +348 -0
- package/src/message-bus.ts +607 -0
- package/src/queen-coordinator.ts +2025 -0
- package/src/shared/events.ts +285 -0
- package/src/shared/types.ts +389 -0
- package/src/topology-manager.ts +656 -0
- package/src/types.ts +545 -0
- package/src/unified-coordinator.ts +1844 -0
- package/src/workers/index.ts +65 -0
- package/src/workers/worker-dispatch.d.ts +234 -0
- package/src/workers/worker-dispatch.d.ts.map +1 -0
- package/src/workers/worker-dispatch.js +842 -0
- package/src/workers/worker-dispatch.js.map +1 -0
- package/src/workers/worker-dispatch.ts +1076 -0
- package/tmp.json +0 -0
- package/tsconfig.json +9 -0
- package/vitest.config.ts +20 -0
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consensus Algorithms Tests
|
|
3
|
+
* Comprehensive tests for Raft, Byzantine, and Gossip consensus
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
7
|
+
import { RaftConsensus, createRaftConsensus } from '../src/consensus/raft.js';
|
|
8
|
+
import { ByzantineConsensus, createByzantineConsensus } from '../src/consensus/byzantine.js';
|
|
9
|
+
import { GossipConsensus, createGossipConsensus } from '../src/consensus/gossip.js';
|
|
10
|
+
import type { ConsensusVote } from '../src/types.js';
|
|
11
|
+
|
|
12
|
+
describe('Raft Consensus', () => {
|
|
13
|
+
let raft: RaftConsensus;
|
|
14
|
+
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
raft = createRaftConsensus('node-1', {
|
|
17
|
+
threshold: 0.66,
|
|
18
|
+
timeoutMs: 5000,
|
|
19
|
+
electionTimeoutMinMs: 50,
|
|
20
|
+
electionTimeoutMaxMs: 100,
|
|
21
|
+
heartbeatIntervalMs: 25,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
await raft.initialize();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(async () => {
|
|
28
|
+
await raft.shutdown();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('Initialization', () => {
|
|
32
|
+
it('should initialize as follower', () => {
|
|
33
|
+
expect(raft.getState()).toBe('follower');
|
|
34
|
+
expect(raft.getTerm()).toBe(0);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should not be leader initially', () => {
|
|
38
|
+
expect(raft.isLeader()).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('Leader Election', () => {
|
|
43
|
+
it('should elect itself as leader with no peers', async () => {
|
|
44
|
+
// Wait for election timeout
|
|
45
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
46
|
+
|
|
47
|
+
// With no peers, node becomes candidate or leader
|
|
48
|
+
const state = raft.getState();
|
|
49
|
+
expect(['candidate', 'leader', 'follower']).toContain(state);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should add and remove peers', () => {
|
|
53
|
+
raft.addPeer('peer-1');
|
|
54
|
+
raft.addPeer('peer-2');
|
|
55
|
+
raft.addPeer('peer-3');
|
|
56
|
+
|
|
57
|
+
raft.removePeer('peer-2');
|
|
58
|
+
|
|
59
|
+
// Verify peers are managed
|
|
60
|
+
expect(() => raft.addPeer('peer-4')).not.toThrow();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should handle vote requests', () => {
|
|
64
|
+
const granted = raft.handleVoteRequest(
|
|
65
|
+
'candidate-1',
|
|
66
|
+
1, // Higher term
|
|
67
|
+
0, // lastLogIndex
|
|
68
|
+
0 // lastLogTerm
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
expect(granted).toBe(true);
|
|
72
|
+
expect(raft.getTerm()).toBe(1);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should reject vote for lower term', () => {
|
|
76
|
+
raft.handleVoteRequest('candidate-1', 5, 0, 0);
|
|
77
|
+
|
|
78
|
+
const granted = raft.handleVoteRequest(
|
|
79
|
+
'candidate-2',
|
|
80
|
+
3, // Lower term
|
|
81
|
+
0,
|
|
82
|
+
0
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
expect(granted).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('Log Replication', () => {
|
|
90
|
+
beforeEach(() => {
|
|
91
|
+
// Make this node leader
|
|
92
|
+
raft.addPeer('peer-1');
|
|
93
|
+
raft.addPeer('peer-2');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should propose value as leader', async () => {
|
|
97
|
+
// Simulate becoming leader
|
|
98
|
+
const raftLeader = createRaftConsensus('leader-node', {
|
|
99
|
+
electionTimeoutMinMs: 50,
|
|
100
|
+
electionTimeoutMaxMs: 100,
|
|
101
|
+
});
|
|
102
|
+
await raftLeader.initialize();
|
|
103
|
+
|
|
104
|
+
// Wait for self-election
|
|
105
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
106
|
+
|
|
107
|
+
if (raftLeader.isLeader()) {
|
|
108
|
+
const proposal = await raftLeader.propose({ value: 'test-data' });
|
|
109
|
+
|
|
110
|
+
expect(proposal).toBeDefined();
|
|
111
|
+
expect(proposal.id).toContain('raft_');
|
|
112
|
+
expect(proposal.value).toEqual({ value: 'test-data' });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
await raftLeader.shutdown();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should reject proposal from non-leader', async () => {
|
|
119
|
+
await expect(
|
|
120
|
+
raft.propose({ value: 'test' })
|
|
121
|
+
).rejects.toThrow('Only leader can propose values');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should handle append entries from leader', () => {
|
|
125
|
+
const success = raft.handleAppendEntries(
|
|
126
|
+
'leader-1',
|
|
127
|
+
1, // Higher term
|
|
128
|
+
[],
|
|
129
|
+
0
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
expect(success).toBe(true);
|
|
133
|
+
expect(raft.getTerm()).toBe(1);
|
|
134
|
+
expect(raft.getState()).toBe('follower');
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe('Consensus Process', () => {
|
|
139
|
+
it('should vote on proposal', async () => {
|
|
140
|
+
raft.addPeer('peer-1');
|
|
141
|
+
raft.addPeer('peer-2');
|
|
142
|
+
|
|
143
|
+
const raftLeader = createRaftConsensus('leader', {});
|
|
144
|
+
await raftLeader.initialize();
|
|
145
|
+
raftLeader.addPeer('node-1');
|
|
146
|
+
|
|
147
|
+
// Simulate leader election
|
|
148
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
149
|
+
|
|
150
|
+
if (raftLeader.isLeader()) {
|
|
151
|
+
const proposal = await raftLeader.propose({ action: 'commit' });
|
|
152
|
+
|
|
153
|
+
const vote: ConsensusVote = {
|
|
154
|
+
voterId: 'node-1',
|
|
155
|
+
approve: true,
|
|
156
|
+
confidence: 1.0,
|
|
157
|
+
timestamp: new Date(),
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
await raftLeader.vote(proposal.id, vote);
|
|
161
|
+
|
|
162
|
+
// Proposal should have the vote
|
|
163
|
+
const result = await raftLeader.awaitConsensus(proposal.id);
|
|
164
|
+
expect(result.proposalId).toBe(proposal.id);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
await raftLeader.shutdown();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should timeout on consensus', async () => {
|
|
171
|
+
const shortTimeout = createRaftConsensus('timeout-node', {
|
|
172
|
+
timeoutMs: 100,
|
|
173
|
+
});
|
|
174
|
+
await shortTimeout.initialize();
|
|
175
|
+
|
|
176
|
+
// Test timeout behavior with invalid proposal
|
|
177
|
+
await expect(
|
|
178
|
+
shortTimeout.awaitConsensus('non-existent-proposal')
|
|
179
|
+
).rejects.toThrow('Proposal non-existent-proposal not found');
|
|
180
|
+
|
|
181
|
+
await shortTimeout.shutdown();
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe('Byzantine Consensus', () => {
|
|
187
|
+
let byzantine: ByzantineConsensus;
|
|
188
|
+
|
|
189
|
+
beforeEach(async () => {
|
|
190
|
+
byzantine = createByzantineConsensus('node-1', {
|
|
191
|
+
threshold: 0.66,
|
|
192
|
+
timeoutMs: 5000,
|
|
193
|
+
maxFaultyNodes: 1,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
await byzantine.initialize();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
afterEach(async () => {
|
|
200
|
+
await byzantine.shutdown();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe('Initialization', () => {
|
|
204
|
+
it('should initialize successfully', () => {
|
|
205
|
+
expect(byzantine.getViewNumber()).toBe(0);
|
|
206
|
+
expect(byzantine.getSequenceNumber()).toBe(0);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should not be primary initially', () => {
|
|
210
|
+
expect(byzantine.isPrimary()).toBe(false);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should calculate max faulty nodes', () => {
|
|
214
|
+
byzantine.addNode('node-2');
|
|
215
|
+
byzantine.addNode('node-3');
|
|
216
|
+
byzantine.addNode('node-4');
|
|
217
|
+
|
|
218
|
+
// With 4 nodes, can tolerate 1 faulty node: f = (n-1)/3 = (4-1)/3 = 1
|
|
219
|
+
expect(byzantine.getMaxFaultyNodes()).toBe(1);
|
|
220
|
+
expect(byzantine.canTolerate(1)).toBe(true);
|
|
221
|
+
expect(byzantine.canTolerate(2)).toBe(false);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe('Primary Election', () => {
|
|
226
|
+
it('should elect primary', () => {
|
|
227
|
+
byzantine.addNode('node-2');
|
|
228
|
+
byzantine.addNode('node-3');
|
|
229
|
+
byzantine.addNode('node-4');
|
|
230
|
+
|
|
231
|
+
const primaryId = byzantine.electPrimary();
|
|
232
|
+
|
|
233
|
+
expect(primaryId).toBeDefined();
|
|
234
|
+
expect(['node-1', 'node-2', 'node-3', 'node-4']).toContain(primaryId);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should rotate primary on view change', async () => {
|
|
238
|
+
byzantine.addNode('node-2');
|
|
239
|
+
byzantine.addNode('node-3');
|
|
240
|
+
|
|
241
|
+
const firstPrimary = byzantine.electPrimary();
|
|
242
|
+
const firstView = byzantine.getViewNumber();
|
|
243
|
+
|
|
244
|
+
await byzantine.initiateViewChange();
|
|
245
|
+
|
|
246
|
+
const secondView = byzantine.getViewNumber();
|
|
247
|
+
expect(secondView).toBe(firstView + 1);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
describe('Three-Phase Commit', () => {
|
|
252
|
+
beforeEach(() => {
|
|
253
|
+
byzantine.addNode('node-2');
|
|
254
|
+
byzantine.addNode('node-3');
|
|
255
|
+
byzantine.addNode('node-4');
|
|
256
|
+
byzantine.addNode('node-1', true); // Make node-1 primary
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('should propose value as primary', async () => {
|
|
260
|
+
const proposal = await byzantine.propose({ data: 'test-value' });
|
|
261
|
+
|
|
262
|
+
expect(proposal).toBeDefined();
|
|
263
|
+
expect(proposal.id).toContain('bft_');
|
|
264
|
+
expect(proposal.value).toEqual({ data: 'test-value' });
|
|
265
|
+
expect(proposal.status).toBe('pending');
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('should reject proposal from non-primary', async () => {
|
|
269
|
+
const nonPrimary = createByzantineConsensus('non-primary', {});
|
|
270
|
+
await nonPrimary.initialize();
|
|
271
|
+
|
|
272
|
+
await expect(
|
|
273
|
+
nonPrimary.propose({ value: 'test' })
|
|
274
|
+
).rejects.toThrow('Only primary can propose values');
|
|
275
|
+
|
|
276
|
+
await nonPrimary.shutdown();
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('should process pre-prepare message', async () => {
|
|
280
|
+
const proposal = await byzantine.propose({ action: 'update' });
|
|
281
|
+
|
|
282
|
+
await byzantine.handlePrePrepare({
|
|
283
|
+
type: 'pre-prepare',
|
|
284
|
+
viewNumber: byzantine.getViewNumber(),
|
|
285
|
+
sequenceNumber: byzantine.getSequenceNumber(),
|
|
286
|
+
digest: 'test-digest',
|
|
287
|
+
senderId: 'node-1',
|
|
288
|
+
timestamp: new Date(),
|
|
289
|
+
payload: { action: 'update' },
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
expect(byzantine.getSequenceNumber()).toBeGreaterThan(0);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('should process prepare message', async () => {
|
|
296
|
+
await byzantine.handlePrepare({
|
|
297
|
+
type: 'prepare',
|
|
298
|
+
viewNumber: byzantine.getViewNumber(),
|
|
299
|
+
sequenceNumber: 1,
|
|
300
|
+
digest: 'test-digest',
|
|
301
|
+
senderId: 'node-2',
|
|
302
|
+
timestamp: new Date(),
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
expect(byzantine.getPreparedCount()).toBeGreaterThanOrEqual(0);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('should process commit message', async () => {
|
|
309
|
+
await byzantine.handleCommit({
|
|
310
|
+
type: 'commit',
|
|
311
|
+
viewNumber: byzantine.getViewNumber(),
|
|
312
|
+
sequenceNumber: 1,
|
|
313
|
+
digest: 'test-digest',
|
|
314
|
+
senderId: 'node-2',
|
|
315
|
+
timestamp: new Date(),
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
expect(byzantine.getCommittedCount()).toBeGreaterThanOrEqual(0);
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
describe('Fault Tolerance', () => {
|
|
323
|
+
it('should achieve consensus with 2f+1 votes', async () => {
|
|
324
|
+
// 4 nodes can tolerate 1 faulty (f=1, need 2*1+1 = 3 votes)
|
|
325
|
+
byzantine.addNode('node-2');
|
|
326
|
+
byzantine.addNode('node-3');
|
|
327
|
+
byzantine.addNode('node-4');
|
|
328
|
+
byzantine.addNode('node-1', true);
|
|
329
|
+
|
|
330
|
+
const proposal = await byzantine.propose({ value: 42 });
|
|
331
|
+
|
|
332
|
+
// Simulate votes from 3 nodes (2f+1)
|
|
333
|
+
const vote: ConsensusVote = {
|
|
334
|
+
voterId: 'node-2',
|
|
335
|
+
approve: true,
|
|
336
|
+
confidence: 1.0,
|
|
337
|
+
timestamp: new Date(),
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
await byzantine.vote(proposal.id, vote);
|
|
341
|
+
|
|
342
|
+
// Check if we need more votes
|
|
343
|
+
const result = await byzantine.awaitConsensus(proposal.id);
|
|
344
|
+
expect(result.proposalId).toBe(proposal.id);
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
describe('Gossip Consensus', () => {
|
|
350
|
+
let gossip: GossipConsensus;
|
|
351
|
+
|
|
352
|
+
beforeEach(async () => {
|
|
353
|
+
gossip = createGossipConsensus('node-1', {
|
|
354
|
+
threshold: 0.66,
|
|
355
|
+
timeoutMs: 5000,
|
|
356
|
+
fanout: 3,
|
|
357
|
+
gossipIntervalMs: 50,
|
|
358
|
+
maxHops: 10,
|
|
359
|
+
convergenceThreshold: 0.9,
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
await gossip.initialize();
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
afterEach(async () => {
|
|
366
|
+
await gossip.shutdown();
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
describe('Initialization', () => {
|
|
370
|
+
it('should initialize successfully', () => {
|
|
371
|
+
expect(gossip.getVersion()).toBe(0);
|
|
372
|
+
expect(gossip.getNeighborCount()).toBe(0);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('should track seen messages', () => {
|
|
376
|
+
expect(gossip.getSeenMessageCount()).toBe(0);
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
describe('Neighbor Management', () => {
|
|
381
|
+
it('should add and remove nodes', () => {
|
|
382
|
+
gossip.addNode('node-2');
|
|
383
|
+
gossip.addNode('node-3');
|
|
384
|
+
gossip.addNode('node-4');
|
|
385
|
+
|
|
386
|
+
gossip.removeNode('node-3');
|
|
387
|
+
|
|
388
|
+
expect(() => gossip.addNeighbor('node-2')).not.toThrow();
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it('should add specific neighbors', () => {
|
|
392
|
+
gossip.addNode('node-2');
|
|
393
|
+
gossip.addNeighbor('node-2');
|
|
394
|
+
|
|
395
|
+
expect(gossip.getNeighborCount()).toBeGreaterThan(0);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it('should remove neighbors', () => {
|
|
399
|
+
gossip.addNode('node-2');
|
|
400
|
+
gossip.addNeighbor('node-2');
|
|
401
|
+
|
|
402
|
+
gossip.removeNeighbor('node-2');
|
|
403
|
+
|
|
404
|
+
// Neighbor count might not be exactly 0 due to random mesh
|
|
405
|
+
expect(() => gossip.getNeighborCount()).not.toThrow();
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
describe('Gossip Protocol', () => {
|
|
410
|
+
beforeEach(() => {
|
|
411
|
+
gossip.addNode('node-2');
|
|
412
|
+
gossip.addNode('node-3');
|
|
413
|
+
gossip.addNode('node-4');
|
|
414
|
+
gossip.addNeighbor('node-2');
|
|
415
|
+
gossip.addNeighbor('node-3');
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it('should propose value', async () => {
|
|
419
|
+
const proposal = await gossip.propose({ message: 'hello-gossip' });
|
|
420
|
+
|
|
421
|
+
expect(proposal).toBeDefined();
|
|
422
|
+
expect(proposal.id).toContain('gossip_');
|
|
423
|
+
expect(proposal.value).toEqual({ message: 'hello-gossip' });
|
|
424
|
+
expect(proposal.status).toBe('pending');
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it('should vote on proposal', async () => {
|
|
428
|
+
const proposal = await gossip.propose({ value: 123 });
|
|
429
|
+
|
|
430
|
+
const vote: ConsensusVote = {
|
|
431
|
+
voterId: 'node-2',
|
|
432
|
+
approve: true,
|
|
433
|
+
confidence: 0.95,
|
|
434
|
+
timestamp: new Date(),
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
await gossip.vote(proposal.id, vote);
|
|
438
|
+
|
|
439
|
+
// Vote should be recorded
|
|
440
|
+
expect(gossip.getConvergence(proposal.id)).toBeGreaterThan(0);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it('should track message queue', async () => {
|
|
444
|
+
await gossip.propose({ data: 'test' });
|
|
445
|
+
|
|
446
|
+
expect(gossip.getQueueDepth()).toBeGreaterThanOrEqual(0);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it('should perform anti-entropy', async () => {
|
|
450
|
+
gossip.addNeighbor('node-2');
|
|
451
|
+
|
|
452
|
+
await expect(gossip.antiEntropy()).resolves.not.toThrow();
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
describe('Convergence', () => {
|
|
457
|
+
it('should calculate convergence rate', async () => {
|
|
458
|
+
gossip.addNode('node-2');
|
|
459
|
+
gossip.addNode('node-3');
|
|
460
|
+
gossip.addNode('node-4');
|
|
461
|
+
|
|
462
|
+
const proposal = await gossip.propose({ value: 'converge' });
|
|
463
|
+
|
|
464
|
+
// Initial convergence (only self-vote)
|
|
465
|
+
const initialConvergence = gossip.getConvergence(proposal.id);
|
|
466
|
+
expect(initialConvergence).toBeGreaterThan(0);
|
|
467
|
+
|
|
468
|
+
// Add more votes
|
|
469
|
+
await gossip.vote(proposal.id, {
|
|
470
|
+
voterId: 'node-2',
|
|
471
|
+
approve: true,
|
|
472
|
+
confidence: 1.0,
|
|
473
|
+
timestamp: new Date(),
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
const updatedConvergence = gossip.getConvergence(proposal.id);
|
|
477
|
+
expect(updatedConvergence).toBeGreaterThanOrEqual(initialConvergence);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it('should achieve eventual consensus', async () => {
|
|
481
|
+
gossip.addNode('node-2');
|
|
482
|
+
gossip.addNode('node-3');
|
|
483
|
+
gossip.addNode('node-4');
|
|
484
|
+
|
|
485
|
+
const proposal = await gossip.propose({ action: 'commit' });
|
|
486
|
+
|
|
487
|
+
// Vote from majority
|
|
488
|
+
await gossip.vote(proposal.id, {
|
|
489
|
+
voterId: 'node-2',
|
|
490
|
+
approve: true,
|
|
491
|
+
confidence: 1.0,
|
|
492
|
+
timestamp: new Date(),
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
await gossip.vote(proposal.id, {
|
|
496
|
+
voterId: 'node-3',
|
|
497
|
+
approve: true,
|
|
498
|
+
confidence: 1.0,
|
|
499
|
+
timestamp: new Date(),
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
await gossip.vote(proposal.id, {
|
|
503
|
+
voterId: 'node-4',
|
|
504
|
+
approve: true,
|
|
505
|
+
confidence: 1.0,
|
|
506
|
+
timestamp: new Date(),
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// Wait for convergence
|
|
510
|
+
const result = await gossip.awaitConsensus(proposal.id);
|
|
511
|
+
|
|
512
|
+
expect(result.proposalId).toBe(proposal.id);
|
|
513
|
+
expect(result.participationRate).toBeGreaterThan(0.5);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it('should handle timeout gracefully', async () => {
|
|
517
|
+
const shortGossip = createGossipConsensus('timeout-node', {
|
|
518
|
+
timeoutMs: 100,
|
|
519
|
+
convergenceThreshold: 0.99, // Very high threshold
|
|
520
|
+
});
|
|
521
|
+
await shortGossip.initialize();
|
|
522
|
+
|
|
523
|
+
const proposal = await shortGossip.propose({ value: 'timeout-test' });
|
|
524
|
+
|
|
525
|
+
// Should timeout and still return result
|
|
526
|
+
const result = await shortGossip.awaitConsensus(proposal.id);
|
|
527
|
+
|
|
528
|
+
expect(result.proposalId).toBe(proposal.id);
|
|
529
|
+
|
|
530
|
+
await shortGossip.shutdown();
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
describe('Message Propagation', () => {
|
|
535
|
+
it('should increment version on propose', async () => {
|
|
536
|
+
const initialVersion = gossip.getVersion();
|
|
537
|
+
|
|
538
|
+
await gossip.propose({ data: 'version-test' });
|
|
539
|
+
|
|
540
|
+
expect(gossip.getVersion()).toBeGreaterThan(initialVersion);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it('should track gossip rounds', async () => {
|
|
544
|
+
const proposal = await gossip.propose({ rounds: 'test' });
|
|
545
|
+
|
|
546
|
+
// Allow some gossip rounds to occur
|
|
547
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
548
|
+
|
|
549
|
+
expect(gossip.getVersion()).toBeGreaterThan(0);
|
|
550
|
+
});
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
describe('Consensus Algorithm Comparison', () => {
|
|
555
|
+
it('should handle different consensus algorithms', async () => {
|
|
556
|
+
const raft = createRaftConsensus('raft-node', {});
|
|
557
|
+
const byzantine = createByzantineConsensus('bft-node', {});
|
|
558
|
+
const gossip = createGossipConsensus('gossip-node', {});
|
|
559
|
+
|
|
560
|
+
await Promise.all([
|
|
561
|
+
raft.initialize(),
|
|
562
|
+
byzantine.initialize(),
|
|
563
|
+
gossip.initialize(),
|
|
564
|
+
]);
|
|
565
|
+
|
|
566
|
+
// All should initialize successfully
|
|
567
|
+
expect(raft.getState()).toBeDefined();
|
|
568
|
+
expect(byzantine.getViewNumber()).toBeDefined();
|
|
569
|
+
expect(gossip.getVersion()).toBeDefined();
|
|
570
|
+
|
|
571
|
+
await Promise.all([
|
|
572
|
+
raft.shutdown(),
|
|
573
|
+
byzantine.shutdown(),
|
|
574
|
+
gossip.shutdown(),
|
|
575
|
+
]);
|
|
576
|
+
});
|
|
577
|
+
});
|