@ruvector/edge-net 0.4.2 → 0.4.4
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/deploy/.env.example +97 -0
- package/deploy/DEPLOY.md +481 -0
- package/deploy/Dockerfile +100 -0
- package/deploy/docker-compose.yml +162 -0
- package/deploy/genesis-prod.js +1550 -0
- package/deploy/health-check.js +187 -0
- package/deploy/prometheus.yml +38 -0
- package/firebase-signaling.js +57 -2
- package/package.json +8 -1
- package/real-workers.js +9 -4
- package/scheduler.js +8 -4
- package/tests/distributed-workers-test.js +1609 -0
- package/tests/multitenancy-test.js +130 -0
- package/tests/p2p-migration-test.js +1102 -0
- package/tests/task-execution-test.js +534 -0
- package/tests/webrtc-peer-test.js +686 -0
- package/webrtc.js +693 -40
|
@@ -0,0 +1,1102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* P2P Migration Test Suite
|
|
4
|
+
*
|
|
5
|
+
* Tests the HybridBootstrap migration flow:
|
|
6
|
+
* firebase -> hybrid -> p2p
|
|
7
|
+
*
|
|
8
|
+
* Validates:
|
|
9
|
+
* 1. Migration thresholds
|
|
10
|
+
* 2. DHT routing table population
|
|
11
|
+
* 3. Signaling fallback behavior
|
|
12
|
+
* 4. Network partition recovery
|
|
13
|
+
* 5. Node churn handling
|
|
14
|
+
*
|
|
15
|
+
* @module @ruvector/edge-net/tests/p2p-migration-test
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { EventEmitter } from 'events';
|
|
19
|
+
import { createHash, randomBytes } from 'crypto';
|
|
20
|
+
|
|
21
|
+
// ============================================
|
|
22
|
+
// MOCK IMPLEMENTATIONS
|
|
23
|
+
// ============================================
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Mock WebRTC Peer Manager for simulation
|
|
27
|
+
*/
|
|
28
|
+
class MockWebRTCPeerManager extends EventEmitter {
|
|
29
|
+
constructor(peerId) {
|
|
30
|
+
super();
|
|
31
|
+
this.peerId = peerId;
|
|
32
|
+
this.peers = new Map();
|
|
33
|
+
this.externalSignaling = null;
|
|
34
|
+
this.stats = {
|
|
35
|
+
totalConnections: 0,
|
|
36
|
+
successfulConnections: 0,
|
|
37
|
+
failedConnections: 0,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
setExternalSignaling(callback) {
|
|
42
|
+
this.externalSignaling = callback;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async connectToPeer(peerId) {
|
|
46
|
+
if (this.peers.has(peerId)) return;
|
|
47
|
+
this.stats.totalConnections++;
|
|
48
|
+
|
|
49
|
+
// Simulate connection delay
|
|
50
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
51
|
+
|
|
52
|
+
// Simulate successful connection
|
|
53
|
+
this.peers.set(peerId, {
|
|
54
|
+
peerId,
|
|
55
|
+
state: 'connected',
|
|
56
|
+
lastSeen: Date.now(),
|
|
57
|
+
});
|
|
58
|
+
this.stats.successfulConnections++;
|
|
59
|
+
this.emit('peer-connected', peerId);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
disconnectPeer(peerId) {
|
|
63
|
+
if (this.peers.has(peerId)) {
|
|
64
|
+
this.peers.delete(peerId);
|
|
65
|
+
this.emit('peer-disconnected', peerId);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
isConnected(peerId) {
|
|
70
|
+
return this.peers.has(peerId);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
sendToPeer(peerId, message) {
|
|
74
|
+
if (this.peers.has(peerId)) {
|
|
75
|
+
this.emit('message-sent', { to: peerId, message });
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async handleOffer({ from, offer }) {
|
|
82
|
+
// Simulate offer handling
|
|
83
|
+
await new Promise(resolve => setTimeout(resolve, 20));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async handleAnswer({ from, answer }) {
|
|
87
|
+
// Simulate answer handling
|
|
88
|
+
await new Promise(resolve => setTimeout(resolve, 20));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async handleIceCandidate({ from, candidate }) {
|
|
92
|
+
// Simulate ICE handling
|
|
93
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
getStats() {
|
|
97
|
+
return {
|
|
98
|
+
...this.stats,
|
|
99
|
+
connectedPeers: this.peers.size,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Mock DHT Node for simulation
|
|
106
|
+
*/
|
|
107
|
+
class MockDHTNode extends EventEmitter {
|
|
108
|
+
constructor(id) {
|
|
109
|
+
super();
|
|
110
|
+
this.id = id || createHash('sha1').update(randomBytes(32)).digest('hex');
|
|
111
|
+
this.peers = new Map();
|
|
112
|
+
this.storage = new Map();
|
|
113
|
+
this.stats = {
|
|
114
|
+
lookups: 0,
|
|
115
|
+
stores: 0,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
addPeer(peer) {
|
|
120
|
+
if (peer.id === this.id) return false;
|
|
121
|
+
this.peers.set(peer.id, { ...peer, lastSeen: Date.now() });
|
|
122
|
+
this.emit('peer-added', peer);
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
removePeer(peerId) {
|
|
127
|
+
if (this.peers.has(peerId)) {
|
|
128
|
+
this.peers.delete(peerId);
|
|
129
|
+
this.emit('peer-removed', peerId);
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
getPeers() {
|
|
136
|
+
return Array.from(this.peers.values());
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
getStats() {
|
|
140
|
+
return {
|
|
141
|
+
...this.stats,
|
|
142
|
+
totalPeers: this.peers.size,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Mock Firebase Signaling for simulation
|
|
149
|
+
*/
|
|
150
|
+
class MockFirebaseSignaling extends EventEmitter {
|
|
151
|
+
constructor(options = {}) {
|
|
152
|
+
super();
|
|
153
|
+
this.peerId = options.peerId;
|
|
154
|
+
this.isConnected = false;
|
|
155
|
+
this.peers = new Map();
|
|
156
|
+
this.stats = {
|
|
157
|
+
firebaseSignals: 0,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async connect() {
|
|
162
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
163
|
+
this.isConnected = true;
|
|
164
|
+
this.emit('connected');
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async disconnect() {
|
|
169
|
+
this.isConnected = false;
|
|
170
|
+
this.peers.clear();
|
|
171
|
+
this.emit('disconnected');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
addPeer(peerId, data = {}) {
|
|
175
|
+
this.peers.set(peerId, { peerId, ...data, lastSeen: Date.now() });
|
|
176
|
+
this.emit('peer-discovered', { peerId, ...data });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
removePeer(peerId) {
|
|
180
|
+
if (this.peers.has(peerId)) {
|
|
181
|
+
this.peers.delete(peerId);
|
|
182
|
+
this.emit('peer-left', { peerId });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async sendOffer(toPeerId, offer) {
|
|
187
|
+
this.stats.firebaseSignals++;
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async sendAnswer(toPeerId, answer) {
|
|
192
|
+
this.stats.firebaseSignals++;
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async sendIceCandidate(toPeerId, candidate) {
|
|
197
|
+
this.stats.firebaseSignals++;
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async sendSignal(toPeerId, type, data) {
|
|
202
|
+
this.stats.firebaseSignals++;
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Simulated HybridBootstrap for testing (mirrors real implementation)
|
|
209
|
+
*/
|
|
210
|
+
class SimulatedHybridBootstrap extends EventEmitter {
|
|
211
|
+
constructor(options = {}) {
|
|
212
|
+
super();
|
|
213
|
+
this.peerId = options.peerId || createHash('sha1').update(randomBytes(16)).digest('hex');
|
|
214
|
+
this.mode = 'firebase';
|
|
215
|
+
this.dhtPeerThreshold = options.dhtPeerThreshold || 5;
|
|
216
|
+
this.p2pPeerThreshold = options.p2pPeerThreshold || 10;
|
|
217
|
+
|
|
218
|
+
// Components
|
|
219
|
+
this.firebase = null;
|
|
220
|
+
this.dht = null;
|
|
221
|
+
this.webrtc = null;
|
|
222
|
+
|
|
223
|
+
// Stats
|
|
224
|
+
this.stats = {
|
|
225
|
+
firebaseDiscoveries: 0,
|
|
226
|
+
dhtDiscoveries: 0,
|
|
227
|
+
directConnections: 0,
|
|
228
|
+
firebaseSignals: 0,
|
|
229
|
+
p2pSignals: 0,
|
|
230
|
+
modeTransitions: [],
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// Migration timing
|
|
234
|
+
this.migrationTimestamps = {
|
|
235
|
+
started: null,
|
|
236
|
+
toHybrid: null,
|
|
237
|
+
toP2P: null,
|
|
238
|
+
fallbackToHybrid: null,
|
|
239
|
+
fallbackToFirebase: null,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async start(webrtc, dht) {
|
|
244
|
+
this.webrtc = webrtc;
|
|
245
|
+
this.dht = dht;
|
|
246
|
+
this.migrationTimestamps.started = Date.now();
|
|
247
|
+
|
|
248
|
+
// Create mock Firebase
|
|
249
|
+
this.firebase = new MockFirebaseSignaling({ peerId: this.peerId });
|
|
250
|
+
this.setupFirebaseEvents();
|
|
251
|
+
|
|
252
|
+
// Set up WebRTC external signaling
|
|
253
|
+
if (this.webrtc) {
|
|
254
|
+
this.webrtc.setExternalSignaling(async (type, toPeerId, data) => {
|
|
255
|
+
switch (type) {
|
|
256
|
+
case 'offer':
|
|
257
|
+
await this.firebase.sendOffer(toPeerId, data);
|
|
258
|
+
break;
|
|
259
|
+
case 'answer':
|
|
260
|
+
await this.firebase.sendAnswer(toPeerId, data);
|
|
261
|
+
break;
|
|
262
|
+
case 'ice-candidate':
|
|
263
|
+
await this.firebase.sendIceCandidate(toPeerId, data);
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
this.stats.firebaseSignals++;
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const connected = await this.firebase.connect();
|
|
271
|
+
if (connected) {
|
|
272
|
+
this.mode = 'firebase';
|
|
273
|
+
this.stats.modeTransitions.push({ from: null, to: 'firebase', timestamp: Date.now() });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return connected;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
setupFirebaseEvents() {
|
|
280
|
+
this.firebase.on('peer-discovered', async ({ peerId }) => {
|
|
281
|
+
this.stats.firebaseDiscoveries++;
|
|
282
|
+
if (this.webrtc) {
|
|
283
|
+
await this.connectToPeer(peerId);
|
|
284
|
+
}
|
|
285
|
+
this.emit('peer-discovered', { peerId, source: 'firebase' });
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
this.firebase.on('offer', async ({ from, offer }) => {
|
|
289
|
+
this.stats.firebaseSignals++;
|
|
290
|
+
if (this.webrtc) {
|
|
291
|
+
await this.webrtc.handleOffer({ from, offer });
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
this.firebase.on('answer', async ({ from, answer }) => {
|
|
296
|
+
this.stats.firebaseSignals++;
|
|
297
|
+
if (this.webrtc) {
|
|
298
|
+
await this.webrtc.handleAnswer({ from, answer });
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async connectToPeer(peerId) {
|
|
304
|
+
if (!this.webrtc) return;
|
|
305
|
+
try {
|
|
306
|
+
await this.webrtc.connectToPeer(peerId);
|
|
307
|
+
this.stats.directConnections++;
|
|
308
|
+
|
|
309
|
+
// Also add to DHT
|
|
310
|
+
if (this.dht) {
|
|
311
|
+
this.dht.addPeer({ id: peerId, lastSeen: Date.now() });
|
|
312
|
+
}
|
|
313
|
+
} catch (error) {
|
|
314
|
+
// Connection failed
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async signal(toPeerId, type, data) {
|
|
319
|
+
if (this.webrtc?.isConnected(toPeerId)) {
|
|
320
|
+
this.webrtc.sendToPeer(toPeerId, { type, data });
|
|
321
|
+
this.stats.p2pSignals++;
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (this.firebase?.isConnected) {
|
|
325
|
+
await this.firebase.sendSignal(toPeerId, type, data);
|
|
326
|
+
this.stats.firebaseSignals++;
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
throw new Error('No signaling path available');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
checkMigration() {
|
|
333
|
+
const connectedPeers = this.webrtc?.peers?.size || 0;
|
|
334
|
+
const dhtPeers = this.dht?.getPeers?.()?.length || 0;
|
|
335
|
+
const previousMode = this.mode;
|
|
336
|
+
|
|
337
|
+
// Migration logic (mirrors real implementation)
|
|
338
|
+
if (this.mode === 'firebase') {
|
|
339
|
+
if (dhtPeers >= this.dhtPeerThreshold) {
|
|
340
|
+
this.mode = 'hybrid';
|
|
341
|
+
this.migrationTimestamps.toHybrid = Date.now();
|
|
342
|
+
}
|
|
343
|
+
} else if (this.mode === 'hybrid') {
|
|
344
|
+
if (connectedPeers >= this.p2pPeerThreshold) {
|
|
345
|
+
this.mode = 'p2p';
|
|
346
|
+
this.migrationTimestamps.toP2P = Date.now();
|
|
347
|
+
} else if (dhtPeers < this.dhtPeerThreshold / 2) {
|
|
348
|
+
this.mode = 'firebase';
|
|
349
|
+
this.migrationTimestamps.fallbackToFirebase = Date.now();
|
|
350
|
+
}
|
|
351
|
+
} else if (this.mode === 'p2p') {
|
|
352
|
+
if (connectedPeers < this.p2pPeerThreshold / 2) {
|
|
353
|
+
this.mode = 'hybrid';
|
|
354
|
+
this.migrationTimestamps.fallbackToHybrid = Date.now();
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (this.mode !== previousMode) {
|
|
359
|
+
this.stats.modeTransitions.push({
|
|
360
|
+
from: previousMode,
|
|
361
|
+
to: this.mode,
|
|
362
|
+
timestamp: Date.now(),
|
|
363
|
+
dhtPeers,
|
|
364
|
+
connectedPeers,
|
|
365
|
+
});
|
|
366
|
+
this.emit('mode-changed', { from: previousMode, to: this.mode });
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return { previousMode, currentMode: this.mode, dhtPeers, connectedPeers };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
getStats() {
|
|
373
|
+
return {
|
|
374
|
+
mode: this.mode,
|
|
375
|
+
...this.stats,
|
|
376
|
+
firebaseConnected: this.firebase?.isConnected || false,
|
|
377
|
+
firebasePeers: this.firebase?.peers?.size || 0,
|
|
378
|
+
dhtPeers: this.dht?.getPeers?.()?.length || 0,
|
|
379
|
+
directPeers: this.webrtc?.peers?.size || 0,
|
|
380
|
+
migrationTimestamps: this.migrationTimestamps,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async stop() {
|
|
385
|
+
if (this.firebase) {
|
|
386
|
+
await this.firebase.disconnect();
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ============================================
|
|
392
|
+
// TEST UTILITIES
|
|
393
|
+
// ============================================
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Create a test network with multiple nodes
|
|
397
|
+
*/
|
|
398
|
+
function createTestNetwork(nodeCount, options = {}) {
|
|
399
|
+
const nodes = [];
|
|
400
|
+
for (let i = 0; i < nodeCount; i++) {
|
|
401
|
+
const peerId = createHash('sha1').update(`test-node-${i}`).digest('hex');
|
|
402
|
+
const webrtc = new MockWebRTCPeerManager(peerId);
|
|
403
|
+
const dht = new MockDHTNode(peerId);
|
|
404
|
+
const bootstrap = new SimulatedHybridBootstrap({
|
|
405
|
+
peerId,
|
|
406
|
+
dhtPeerThreshold: options.dhtPeerThreshold || 5,
|
|
407
|
+
p2pPeerThreshold: options.p2pPeerThreshold || 10,
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
nodes.push({
|
|
411
|
+
id: i,
|
|
412
|
+
peerId,
|
|
413
|
+
webrtc,
|
|
414
|
+
dht,
|
|
415
|
+
bootstrap,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
return nodes;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Simulate peer discovery via Firebase
|
|
423
|
+
*/
|
|
424
|
+
async function simulateFirebaseDiscovery(nodes) {
|
|
425
|
+
for (const node of nodes) {
|
|
426
|
+
for (const otherNode of nodes) {
|
|
427
|
+
if (node.peerId !== otherNode.peerId) {
|
|
428
|
+
node.bootstrap.firebase.addPeer(otherNode.peerId);
|
|
429
|
+
await new Promise(r => setTimeout(r, 10));
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Simulate nodes joining the network gradually
|
|
437
|
+
*/
|
|
438
|
+
async function simulateGradualJoin(nodes, delayMs = 100) {
|
|
439
|
+
for (const node of nodes) {
|
|
440
|
+
await node.bootstrap.start(node.webrtc, node.dht);
|
|
441
|
+
await new Promise(r => setTimeout(r, delayMs));
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Connect nodes directly (simulate WebRTC connections)
|
|
447
|
+
*/
|
|
448
|
+
async function connectNodes(nodeA, nodeB) {
|
|
449
|
+
await nodeA.webrtc.connectToPeer(nodeB.peerId);
|
|
450
|
+
await nodeB.webrtc.connectToPeer(nodeA.peerId);
|
|
451
|
+
nodeA.dht.addPeer({ id: nodeB.peerId });
|
|
452
|
+
nodeB.dht.addPeer({ id: nodeA.peerId });
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Test result formatter
|
|
457
|
+
*/
|
|
458
|
+
function formatTestResult(name, passed, details = {}) {
|
|
459
|
+
const status = passed ? '\x1b[32mPASSED\x1b[0m' : '\x1b[31mFAILED\x1b[0m';
|
|
460
|
+
console.log(` ${status}: ${name}`);
|
|
461
|
+
if (!passed && Object.keys(details).length > 0) {
|
|
462
|
+
console.log(' Details:', JSON.stringify(details, null, 2));
|
|
463
|
+
}
|
|
464
|
+
return passed;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// ============================================
|
|
468
|
+
// TEST SCENARIOS
|
|
469
|
+
// ============================================
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* TEST 1: Happy Path - Gradual Network Growth
|
|
473
|
+
*
|
|
474
|
+
* Scenario: Nodes join gradually, network grows, migration occurs
|
|
475
|
+
* Expected: firebase -> hybrid -> p2p transitions
|
|
476
|
+
*/
|
|
477
|
+
async function testHappyPathGradualGrowth() {
|
|
478
|
+
console.log('\n--- TEST 1: Happy Path - Gradual Network Growth ---');
|
|
479
|
+
|
|
480
|
+
const nodes = createTestNetwork(15, {
|
|
481
|
+
dhtPeerThreshold: 3,
|
|
482
|
+
p2pPeerThreshold: 8,
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// Start first node
|
|
486
|
+
const firstNode = nodes[0];
|
|
487
|
+
await firstNode.bootstrap.start(firstNode.webrtc, firstNode.dht);
|
|
488
|
+
|
|
489
|
+
let passed = true;
|
|
490
|
+
|
|
491
|
+
// Verify starts in firebase mode
|
|
492
|
+
passed = formatTestResult(
|
|
493
|
+
'Node starts in firebase mode',
|
|
494
|
+
firstNode.bootstrap.mode === 'firebase',
|
|
495
|
+
{ mode: firstNode.bootstrap.mode }
|
|
496
|
+
) && passed;
|
|
497
|
+
|
|
498
|
+
// Add nodes gradually and check transitions
|
|
499
|
+
for (let i = 1; i < nodes.length; i++) {
|
|
500
|
+
const node = nodes[i];
|
|
501
|
+
await node.bootstrap.start(node.webrtc, node.dht);
|
|
502
|
+
|
|
503
|
+
// Connect to first node
|
|
504
|
+
await connectNodes(firstNode, node);
|
|
505
|
+
|
|
506
|
+
// Check migration
|
|
507
|
+
firstNode.bootstrap.checkMigration();
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Final state checks
|
|
511
|
+
const stats = firstNode.bootstrap.getStats();
|
|
512
|
+
|
|
513
|
+
passed = formatTestResult(
|
|
514
|
+
'Transitioned to hybrid mode',
|
|
515
|
+
stats.modeTransitions.some(t => t.to === 'hybrid'),
|
|
516
|
+
{ transitions: stats.modeTransitions }
|
|
517
|
+
) && passed;
|
|
518
|
+
|
|
519
|
+
passed = formatTestResult(
|
|
520
|
+
'Transitioned to p2p mode',
|
|
521
|
+
stats.mode === 'p2p' || stats.modeTransitions.some(t => t.to === 'p2p'),
|
|
522
|
+
{ currentMode: stats.mode, transitions: stats.modeTransitions }
|
|
523
|
+
) && passed;
|
|
524
|
+
|
|
525
|
+
passed = formatTestResult(
|
|
526
|
+
'DHT routing table populated',
|
|
527
|
+
stats.dhtPeers >= 10,
|
|
528
|
+
{ dhtPeers: stats.dhtPeers }
|
|
529
|
+
) && passed;
|
|
530
|
+
|
|
531
|
+
// Cleanup
|
|
532
|
+
for (const node of nodes) {
|
|
533
|
+
await node.bootstrap.stop();
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return passed;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* TEST 2: Edge Case - Nodes Leaving
|
|
541
|
+
*
|
|
542
|
+
* Scenario: Network grows then nodes leave
|
|
543
|
+
* Expected: p2p -> hybrid -> firebase fallback
|
|
544
|
+
*
|
|
545
|
+
* Fallback thresholds are half the original:
|
|
546
|
+
* - P2P -> Hybrid: when connectedPeers < p2pPeerThreshold / 2
|
|
547
|
+
* - Hybrid -> Firebase: when dhtPeers < dhtPeerThreshold / 2
|
|
548
|
+
*/
|
|
549
|
+
async function testNodesLeaving() {
|
|
550
|
+
console.log('\n--- TEST 2: Edge Case - Nodes Leaving ---');
|
|
551
|
+
|
|
552
|
+
const nodes = createTestNetwork(12, {
|
|
553
|
+
dhtPeerThreshold: 3,
|
|
554
|
+
p2pPeerThreshold: 6,
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
let passed = true;
|
|
558
|
+
|
|
559
|
+
// Build up network
|
|
560
|
+
for (const node of nodes) {
|
|
561
|
+
await node.bootstrap.start(node.webrtc, node.dht);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Connect all nodes to first node
|
|
565
|
+
const firstNode = nodes[0];
|
|
566
|
+
for (let i = 1; i < nodes.length; i++) {
|
|
567
|
+
await connectNodes(firstNode, nodes[i]);
|
|
568
|
+
firstNode.bootstrap.checkMigration();
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Verify in p2p mode
|
|
572
|
+
passed = formatTestResult(
|
|
573
|
+
'Reached p2p mode with full network',
|
|
574
|
+
firstNode.bootstrap.mode === 'p2p',
|
|
575
|
+
{ mode: firstNode.bootstrap.mode }
|
|
576
|
+
) && passed;
|
|
577
|
+
|
|
578
|
+
// Simulate nodes leaving - need to get below p2pPeerThreshold/2 = 3
|
|
579
|
+
// So we need to disconnect until we have < 3 peers
|
|
580
|
+
for (let i = nodes.length - 1; i >= 4; i--) {
|
|
581
|
+
firstNode.webrtc.disconnectPeer(nodes[i].peerId);
|
|
582
|
+
firstNode.dht.removePeer(nodes[i].peerId);
|
|
583
|
+
firstNode.bootstrap.checkMigration();
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Now we should have 3 peers (indices 1,2,3), still at or above threshold
|
|
587
|
+
// Need to drop one more to trigger fallback
|
|
588
|
+
firstNode.webrtc.disconnectPeer(nodes[3].peerId);
|
|
589
|
+
firstNode.dht.removePeer(nodes[3].peerId);
|
|
590
|
+
firstNode.bootstrap.checkMigration();
|
|
591
|
+
|
|
592
|
+
// Should fall back to hybrid (2 peers < 3)
|
|
593
|
+
passed = formatTestResult(
|
|
594
|
+
'Falls back to hybrid when peers drop below half threshold',
|
|
595
|
+
firstNode.bootstrap.mode === 'hybrid',
|
|
596
|
+
{ mode: firstNode.bootstrap.mode, directPeers: firstNode.webrtc.peers.size, threshold: 'p2pPeerThreshold/2 = 3' }
|
|
597
|
+
) && passed;
|
|
598
|
+
|
|
599
|
+
// More nodes leave - need to get DHT below dhtPeerThreshold/2 = 1.5
|
|
600
|
+
firstNode.webrtc.disconnectPeer(nodes[2].peerId);
|
|
601
|
+
firstNode.dht.removePeer(nodes[2].peerId);
|
|
602
|
+
firstNode.bootstrap.checkMigration();
|
|
603
|
+
|
|
604
|
+
// Now 1 DHT peer, should fall back to firebase (1 < 1.5)
|
|
605
|
+
passed = formatTestResult(
|
|
606
|
+
'Falls back to firebase when DHT peers drop below half threshold',
|
|
607
|
+
firstNode.bootstrap.mode === 'firebase',
|
|
608
|
+
{ mode: firstNode.bootstrap.mode, dhtPeers: firstNode.dht.getPeers().length, threshold: 'dhtPeerThreshold/2 = 1.5' }
|
|
609
|
+
) && passed;
|
|
610
|
+
|
|
611
|
+
// Cleanup
|
|
612
|
+
for (const node of nodes) {
|
|
613
|
+
await node.bootstrap.stop();
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
return passed;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* TEST 3: Edge Case - Nodes Rejoining
|
|
621
|
+
*
|
|
622
|
+
* Scenario: Nodes leave then rejoin
|
|
623
|
+
* Expected: Proper re-migration
|
|
624
|
+
*/
|
|
625
|
+
async function testNodesRejoining() {
|
|
626
|
+
console.log('\n--- TEST 3: Edge Case - Nodes Rejoining ---');
|
|
627
|
+
|
|
628
|
+
const nodes = createTestNetwork(10, {
|
|
629
|
+
dhtPeerThreshold: 3,
|
|
630
|
+
p2pPeerThreshold: 6,
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
let passed = true;
|
|
634
|
+
|
|
635
|
+
// Initial build-up
|
|
636
|
+
const firstNode = nodes[0];
|
|
637
|
+
await firstNode.bootstrap.start(firstNode.webrtc, firstNode.dht);
|
|
638
|
+
|
|
639
|
+
for (let i = 1; i < 8; i++) {
|
|
640
|
+
await nodes[i].bootstrap.start(nodes[i].webrtc, nodes[i].dht);
|
|
641
|
+
await connectNodes(firstNode, nodes[i]);
|
|
642
|
+
firstNode.bootstrap.checkMigration();
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
passed = formatTestResult(
|
|
646
|
+
'Initial p2p state reached',
|
|
647
|
+
firstNode.bootstrap.mode === 'p2p',
|
|
648
|
+
{ mode: firstNode.bootstrap.mode }
|
|
649
|
+
) && passed;
|
|
650
|
+
|
|
651
|
+
// Nodes leave
|
|
652
|
+
for (let i = 7; i >= 2; i--) {
|
|
653
|
+
firstNode.webrtc.disconnectPeer(nodes[i].peerId);
|
|
654
|
+
firstNode.dht.removePeer(nodes[i].peerId);
|
|
655
|
+
}
|
|
656
|
+
firstNode.bootstrap.checkMigration();
|
|
657
|
+
|
|
658
|
+
const modeAfterLeaving = firstNode.bootstrap.mode;
|
|
659
|
+
passed = formatTestResult(
|
|
660
|
+
'Mode degraded after nodes left',
|
|
661
|
+
modeAfterLeaving !== 'p2p',
|
|
662
|
+
{ mode: modeAfterLeaving }
|
|
663
|
+
) && passed;
|
|
664
|
+
|
|
665
|
+
// Nodes rejoin
|
|
666
|
+
for (let i = 2; i < 8; i++) {
|
|
667
|
+
await connectNodes(firstNode, nodes[i]);
|
|
668
|
+
firstNode.bootstrap.checkMigration();
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// New nodes join
|
|
672
|
+
for (let i = 8; i < 10; i++) {
|
|
673
|
+
await nodes[i].bootstrap.start(nodes[i].webrtc, nodes[i].dht);
|
|
674
|
+
await connectNodes(firstNode, nodes[i]);
|
|
675
|
+
firstNode.bootstrap.checkMigration();
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
passed = formatTestResult(
|
|
679
|
+
'Re-migrated to p2p after rejoin',
|
|
680
|
+
firstNode.bootstrap.mode === 'p2p',
|
|
681
|
+
{ mode: firstNode.bootstrap.mode, directPeers: firstNode.webrtc.peers.size }
|
|
682
|
+
) && passed;
|
|
683
|
+
|
|
684
|
+
// Cleanup
|
|
685
|
+
for (const node of nodes) {
|
|
686
|
+
await node.bootstrap.stop();
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
return passed;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* TEST 4: Network Partition Recovery
|
|
694
|
+
*
|
|
695
|
+
* Scenario: Network splits then recovers
|
|
696
|
+
* Expected: Graceful handling of partition
|
|
697
|
+
*/
|
|
698
|
+
async function testNetworkPartitionRecovery() {
|
|
699
|
+
console.log('\n--- TEST 4: Network Partition Recovery ---');
|
|
700
|
+
|
|
701
|
+
const nodes = createTestNetwork(12, {
|
|
702
|
+
dhtPeerThreshold: 3,
|
|
703
|
+
p2pPeerThreshold: 6,
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
let passed = true;
|
|
707
|
+
|
|
708
|
+
// Build full network
|
|
709
|
+
for (const node of nodes) {
|
|
710
|
+
await node.bootstrap.start(node.webrtc, node.dht);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Connect all in mesh
|
|
714
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
715
|
+
for (let j = i + 1; j < nodes.length; j++) {
|
|
716
|
+
await connectNodes(nodes[i], nodes[j]);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Check migration multiple times to allow state to propagate
|
|
721
|
+
// (real implementation has 30s interval, but we check immediately)
|
|
722
|
+
for (let round = 0; round < 3; round++) {
|
|
723
|
+
for (const node of nodes) {
|
|
724
|
+
node.bootstrap.checkMigration();
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// In mesh topology with 11 connections each, all should be in p2p mode
|
|
729
|
+
const p2pCount = nodes.filter(n => n.bootstrap.mode === 'p2p').length;
|
|
730
|
+
passed = formatTestResult(
|
|
731
|
+
'Most nodes in p2p mode initially (mesh has 11 connections each)',
|
|
732
|
+
p2pCount >= 10, // Allow some variance
|
|
733
|
+
{ p2pNodes: p2pCount, totalNodes: nodes.length, modes: nodes.map(n => n.bootstrap.mode) }
|
|
734
|
+
) && passed;
|
|
735
|
+
|
|
736
|
+
// Simulate partition: split into two groups
|
|
737
|
+
const groupA = nodes.slice(0, 6);
|
|
738
|
+
const groupB = nodes.slice(6);
|
|
739
|
+
|
|
740
|
+
// Disconnect cross-group connections
|
|
741
|
+
for (const nodeA of groupA) {
|
|
742
|
+
for (const nodeB of groupB) {
|
|
743
|
+
nodeA.webrtc.disconnectPeer(nodeB.peerId);
|
|
744
|
+
nodeA.dht.removePeer(nodeB.peerId);
|
|
745
|
+
nodeB.webrtc.disconnectPeer(nodeA.peerId);
|
|
746
|
+
nodeB.dht.removePeer(nodeA.peerId);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Check migration for both groups
|
|
751
|
+
for (const node of nodes) {
|
|
752
|
+
node.bootstrap.checkMigration();
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
passed = formatTestResult(
|
|
756
|
+
'Both partitions still operational',
|
|
757
|
+
groupA[0].bootstrap.mode !== 'firebase' && groupB[0].bootstrap.mode !== 'firebase',
|
|
758
|
+
{ groupA: groupA[0].bootstrap.mode, groupB: groupB[0].bootstrap.mode }
|
|
759
|
+
) && passed;
|
|
760
|
+
|
|
761
|
+
// Heal partition
|
|
762
|
+
for (const nodeA of groupA) {
|
|
763
|
+
for (const nodeB of groupB) {
|
|
764
|
+
await connectNodes(nodeA, nodeB);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
for (const node of nodes) {
|
|
769
|
+
node.bootstrap.checkMigration();
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
passed = formatTestResult(
|
|
773
|
+
'Network recovered to p2p after healing',
|
|
774
|
+
nodes.every(n => n.bootstrap.mode === 'p2p'),
|
|
775
|
+
{ modes: nodes.map(n => n.bootstrap.mode) }
|
|
776
|
+
) && passed;
|
|
777
|
+
|
|
778
|
+
// Cleanup
|
|
779
|
+
for (const node of nodes) {
|
|
780
|
+
await node.bootstrap.stop();
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
return passed;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* TEST 5: Signaling Fallback Behavior
|
|
788
|
+
*
|
|
789
|
+
* Scenario: Test signaling routes through correct channel
|
|
790
|
+
* Expected: P2P when available, Firebase fallback
|
|
791
|
+
*/
|
|
792
|
+
async function testSignalingFallback() {
|
|
793
|
+
console.log('\n--- TEST 5: Signaling Fallback Behavior ---');
|
|
794
|
+
|
|
795
|
+
const nodes = createTestNetwork(3, {
|
|
796
|
+
dhtPeerThreshold: 1,
|
|
797
|
+
p2pPeerThreshold: 2,
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
let passed = true;
|
|
801
|
+
|
|
802
|
+
// Start all nodes
|
|
803
|
+
for (const node of nodes) {
|
|
804
|
+
await node.bootstrap.start(node.webrtc, node.dht);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
const nodeA = nodes[0];
|
|
808
|
+
const nodeB = nodes[1];
|
|
809
|
+
const nodeC = nodes[2];
|
|
810
|
+
|
|
811
|
+
// Signal without P2P connection (should use Firebase)
|
|
812
|
+
const statsBeforeFirebase = { ...nodeA.bootstrap.stats };
|
|
813
|
+
await nodeA.bootstrap.signal(nodeB.peerId, 'test', { data: 'hello' });
|
|
814
|
+
|
|
815
|
+
passed = formatTestResult(
|
|
816
|
+
'Uses Firebase for signaling without P2P',
|
|
817
|
+
nodeA.bootstrap.stats.firebaseSignals > statsBeforeFirebase.firebaseSignals,
|
|
818
|
+
{ before: statsBeforeFirebase.firebaseSignals, after: nodeA.bootstrap.stats.firebaseSignals }
|
|
819
|
+
) && passed;
|
|
820
|
+
|
|
821
|
+
// Connect nodes directly
|
|
822
|
+
await connectNodes(nodeA, nodeB);
|
|
823
|
+
|
|
824
|
+
// Signal with P2P connection (should use P2P)
|
|
825
|
+
const statsBeforeP2P = { ...nodeA.bootstrap.stats };
|
|
826
|
+
await nodeA.bootstrap.signal(nodeB.peerId, 'test', { data: 'hello' });
|
|
827
|
+
|
|
828
|
+
passed = formatTestResult(
|
|
829
|
+
'Uses P2P for signaling when connected',
|
|
830
|
+
nodeA.bootstrap.stats.p2pSignals > statsBeforeP2P.p2pSignals,
|
|
831
|
+
{ before: statsBeforeP2P.p2pSignals, after: nodeA.bootstrap.stats.p2pSignals }
|
|
832
|
+
) && passed;
|
|
833
|
+
|
|
834
|
+
// Signal to unconnected peer (should fallback to Firebase)
|
|
835
|
+
const statsBeforeFallback = { ...nodeA.bootstrap.stats };
|
|
836
|
+
await nodeA.bootstrap.signal(nodeC.peerId, 'test', { data: 'hello' });
|
|
837
|
+
|
|
838
|
+
passed = formatTestResult(
|
|
839
|
+
'Falls back to Firebase for unconnected peer',
|
|
840
|
+
nodeA.bootstrap.stats.firebaseSignals > statsBeforeFallback.firebaseSignals,
|
|
841
|
+
{ before: statsBeforeFallback.firebaseSignals, after: nodeA.bootstrap.stats.firebaseSignals }
|
|
842
|
+
) && passed;
|
|
843
|
+
|
|
844
|
+
// Cleanup
|
|
845
|
+
for (const node of nodes) {
|
|
846
|
+
await node.bootstrap.stop();
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
return passed;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* TEST 6: DHT Routing Table Population
|
|
854
|
+
*
|
|
855
|
+
* Scenario: Validate DHT is properly populated during migration
|
|
856
|
+
* Expected: DHT contains all connected peers
|
|
857
|
+
*/
|
|
858
|
+
async function testDHTRoutingTablePopulation() {
|
|
859
|
+
console.log('\n--- TEST 6: DHT Routing Table Population ---');
|
|
860
|
+
|
|
861
|
+
const nodes = createTestNetwork(8, {
|
|
862
|
+
dhtPeerThreshold: 3,
|
|
863
|
+
p2pPeerThreshold: 6,
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
let passed = true;
|
|
867
|
+
|
|
868
|
+
const firstNode = nodes[0];
|
|
869
|
+
await firstNode.bootstrap.start(firstNode.webrtc, firstNode.dht);
|
|
870
|
+
|
|
871
|
+
// Connect nodes and verify DHT population
|
|
872
|
+
for (let i = 1; i < nodes.length; i++) {
|
|
873
|
+
await nodes[i].bootstrap.start(nodes[i].webrtc, nodes[i].dht);
|
|
874
|
+
await connectNodes(firstNode, nodes[i]);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Check DHT stats
|
|
878
|
+
const dhtStats = firstNode.dht.getStats();
|
|
879
|
+
const webrtcStats = firstNode.webrtc.getStats();
|
|
880
|
+
|
|
881
|
+
passed = formatTestResult(
|
|
882
|
+
'DHT peers matches WebRTC connections',
|
|
883
|
+
dhtStats.totalPeers === webrtcStats.connectedPeers,
|
|
884
|
+
{ dhtPeers: dhtStats.totalPeers, webrtcPeers: webrtcStats.connectedPeers }
|
|
885
|
+
) && passed;
|
|
886
|
+
|
|
887
|
+
// Verify all peers are in DHT
|
|
888
|
+
const dhtPeerIds = new Set(firstNode.dht.getPeers().map(p => p.id));
|
|
889
|
+
const allConnected = nodes.slice(1).every(n => dhtPeerIds.has(n.peerId));
|
|
890
|
+
|
|
891
|
+
passed = formatTestResult(
|
|
892
|
+
'All connected peers in DHT routing table',
|
|
893
|
+
allConnected,
|
|
894
|
+
{ dhtPeers: Array.from(dhtPeerIds).map(p => p.slice(0, 8)) }
|
|
895
|
+
) && passed;
|
|
896
|
+
|
|
897
|
+
// Cleanup
|
|
898
|
+
for (const node of nodes) {
|
|
899
|
+
await node.bootstrap.stop();
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
return passed;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* TEST 7: Migration Timing Measurement
|
|
907
|
+
*
|
|
908
|
+
* Scenario: Measure migration timing for performance analysis
|
|
909
|
+
* Expected: Record timing data for analysis
|
|
910
|
+
*/
|
|
911
|
+
async function testMigrationTiming() {
|
|
912
|
+
console.log('\n--- TEST 7: Migration Timing Measurement ---');
|
|
913
|
+
|
|
914
|
+
const nodes = createTestNetwork(15, {
|
|
915
|
+
dhtPeerThreshold: 3,
|
|
916
|
+
p2pPeerThreshold: 8,
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
let passed = true;
|
|
920
|
+
|
|
921
|
+
const firstNode = nodes[0];
|
|
922
|
+
await firstNode.bootstrap.start(firstNode.webrtc, firstNode.dht);
|
|
923
|
+
|
|
924
|
+
// Connect nodes and track timing
|
|
925
|
+
for (let i = 1; i < nodes.length; i++) {
|
|
926
|
+
await nodes[i].bootstrap.start(nodes[i].webrtc, nodes[i].dht);
|
|
927
|
+
await connectNodes(firstNode, nodes[i]);
|
|
928
|
+
firstNode.bootstrap.checkMigration();
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
const stats = firstNode.bootstrap.getStats();
|
|
932
|
+
const timestamps = stats.migrationTimestamps;
|
|
933
|
+
|
|
934
|
+
passed = formatTestResult(
|
|
935
|
+
'Migration timestamps recorded',
|
|
936
|
+
timestamps.started !== null,
|
|
937
|
+
{ timestamps }
|
|
938
|
+
) && passed;
|
|
939
|
+
|
|
940
|
+
if (timestamps.toHybrid) {
|
|
941
|
+
const firebaseToHybridTime = timestamps.toHybrid - timestamps.started;
|
|
942
|
+
console.log(` Firebase -> Hybrid: ${firebaseToHybridTime}ms`);
|
|
943
|
+
|
|
944
|
+
passed = formatTestResult(
|
|
945
|
+
'Firebase to Hybrid migration completed',
|
|
946
|
+
firebaseToHybridTime > 0,
|
|
947
|
+
{ timeMs: firebaseToHybridTime }
|
|
948
|
+
) && passed;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
if (timestamps.toP2P) {
|
|
952
|
+
const hybridToP2PTime = timestamps.toP2P - timestamps.toHybrid;
|
|
953
|
+
const totalTime = timestamps.toP2P - timestamps.started;
|
|
954
|
+
console.log(` Hybrid -> P2P: ${hybridToP2PTime}ms`);
|
|
955
|
+
console.log(` Total migration: ${totalTime}ms`);
|
|
956
|
+
|
|
957
|
+
passed = formatTestResult(
|
|
958
|
+
'Hybrid to P2P migration completed',
|
|
959
|
+
hybridToP2PTime > 0,
|
|
960
|
+
{ timeMs: hybridToP2PTime }
|
|
961
|
+
) && passed;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// Cleanup
|
|
965
|
+
for (const node of nodes) {
|
|
966
|
+
await node.bootstrap.stop();
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
return passed;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/**
|
|
973
|
+
* TEST 8: Threshold Configuration
|
|
974
|
+
*
|
|
975
|
+
* Scenario: Test different threshold configurations
|
|
976
|
+
* Expected: Migration respects configured thresholds
|
|
977
|
+
*/
|
|
978
|
+
async function testThresholdConfiguration() {
|
|
979
|
+
console.log('\n--- TEST 8: Threshold Configuration ---');
|
|
980
|
+
|
|
981
|
+
let passed = true;
|
|
982
|
+
|
|
983
|
+
// Test with low thresholds
|
|
984
|
+
const lowNodes = createTestNetwork(5, {
|
|
985
|
+
dhtPeerThreshold: 2,
|
|
986
|
+
p2pPeerThreshold: 3,
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
const lowFirst = lowNodes[0];
|
|
990
|
+
await lowFirst.bootstrap.start(lowFirst.webrtc, lowFirst.dht);
|
|
991
|
+
|
|
992
|
+
for (let i = 1; i < 4; i++) {
|
|
993
|
+
await lowNodes[i].bootstrap.start(lowNodes[i].webrtc, lowNodes[i].dht);
|
|
994
|
+
await connectNodes(lowFirst, lowNodes[i]);
|
|
995
|
+
lowFirst.bootstrap.checkMigration();
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
passed = formatTestResult(
|
|
999
|
+
'Low thresholds: Reaches P2P with fewer peers',
|
|
1000
|
+
lowFirst.bootstrap.mode === 'p2p',
|
|
1001
|
+
{ mode: lowFirst.bootstrap.mode, peers: lowFirst.webrtc.peers.size }
|
|
1002
|
+
) && passed;
|
|
1003
|
+
|
|
1004
|
+
// Test with high thresholds
|
|
1005
|
+
const highNodes = createTestNetwork(5, {
|
|
1006
|
+
dhtPeerThreshold: 10,
|
|
1007
|
+
p2pPeerThreshold: 20,
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
const highFirst = highNodes[0];
|
|
1011
|
+
await highFirst.bootstrap.start(highFirst.webrtc, highFirst.dht);
|
|
1012
|
+
|
|
1013
|
+
for (let i = 1; i < 5; i++) {
|
|
1014
|
+
await highNodes[i].bootstrap.start(highNodes[i].webrtc, highNodes[i].dht);
|
|
1015
|
+
await connectNodes(highFirst, highNodes[i]);
|
|
1016
|
+
highFirst.bootstrap.checkMigration();
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
passed = formatTestResult(
|
|
1020
|
+
'High thresholds: Stays in firebase with few peers',
|
|
1021
|
+
highFirst.bootstrap.mode === 'firebase',
|
|
1022
|
+
{ mode: highFirst.bootstrap.mode, peers: highFirst.webrtc.peers.size }
|
|
1023
|
+
) && passed;
|
|
1024
|
+
|
|
1025
|
+
// Cleanup
|
|
1026
|
+
for (const node of [...lowNodes, ...highNodes]) {
|
|
1027
|
+
await node.bootstrap.stop();
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
return passed;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// ============================================
|
|
1034
|
+
// TEST RUNNER
|
|
1035
|
+
// ============================================
|
|
1036
|
+
|
|
1037
|
+
async function runAllTests() {
|
|
1038
|
+
console.log('\n' + '='.repeat(60));
|
|
1039
|
+
console.log('P2P MIGRATION TEST SUITE');
|
|
1040
|
+
console.log('='.repeat(60));
|
|
1041
|
+
|
|
1042
|
+
const results = [];
|
|
1043
|
+
|
|
1044
|
+
try {
|
|
1045
|
+
results.push({ name: 'Happy Path - Gradual Growth', passed: await testHappyPathGradualGrowth() });
|
|
1046
|
+
results.push({ name: 'Edge Case - Nodes Leaving', passed: await testNodesLeaving() });
|
|
1047
|
+
results.push({ name: 'Edge Case - Nodes Rejoining', passed: await testNodesRejoining() });
|
|
1048
|
+
results.push({ name: 'Network Partition Recovery', passed: await testNetworkPartitionRecovery() });
|
|
1049
|
+
results.push({ name: 'Signaling Fallback Behavior', passed: await testSignalingFallback() });
|
|
1050
|
+
results.push({ name: 'DHT Routing Table Population', passed: await testDHTRoutingTablePopulation() });
|
|
1051
|
+
results.push({ name: 'Migration Timing Measurement', passed: await testMigrationTiming() });
|
|
1052
|
+
results.push({ name: 'Threshold Configuration', passed: await testThresholdConfiguration() });
|
|
1053
|
+
} catch (error) {
|
|
1054
|
+
console.error('\nTest suite error:', error);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// Summary
|
|
1058
|
+
console.log('\n' + '='.repeat(60));
|
|
1059
|
+
console.log('TEST SUMMARY');
|
|
1060
|
+
console.log('='.repeat(60));
|
|
1061
|
+
|
|
1062
|
+
const passed = results.filter(r => r.passed).length;
|
|
1063
|
+
const failed = results.filter(r => !r.passed).length;
|
|
1064
|
+
|
|
1065
|
+
for (const result of results) {
|
|
1066
|
+
const status = result.passed ? '\x1b[32mPASS\x1b[0m' : '\x1b[31mFAIL\x1b[0m';
|
|
1067
|
+
console.log(` ${status}: ${result.name}`);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
console.log('\n' + '-'.repeat(60));
|
|
1071
|
+
console.log(`Total: ${results.length} | Passed: ${passed} | Failed: ${failed}`);
|
|
1072
|
+
console.log('='.repeat(60) + '\n');
|
|
1073
|
+
|
|
1074
|
+
return failed === 0;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// Run if executed directly
|
|
1078
|
+
if (process.argv[1]?.endsWith('p2p-migration-test.js')) {
|
|
1079
|
+
runAllTests()
|
|
1080
|
+
.then(success => process.exit(success ? 0 : 1))
|
|
1081
|
+
.catch(err => {
|
|
1082
|
+
console.error('Fatal error:', err);
|
|
1083
|
+
process.exit(1);
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
export {
|
|
1088
|
+
runAllTests,
|
|
1089
|
+
testHappyPathGradualGrowth,
|
|
1090
|
+
testNodesLeaving,
|
|
1091
|
+
testNodesRejoining,
|
|
1092
|
+
testNetworkPartitionRecovery,
|
|
1093
|
+
testSignalingFallback,
|
|
1094
|
+
testDHTRoutingTablePopulation,
|
|
1095
|
+
testMigrationTiming,
|
|
1096
|
+
testThresholdConfiguration,
|
|
1097
|
+
SimulatedHybridBootstrap,
|
|
1098
|
+
MockWebRTCPeerManager,
|
|
1099
|
+
MockDHTNode,
|
|
1100
|
+
MockFirebaseSignaling,
|
|
1101
|
+
createTestNetwork,
|
|
1102
|
+
};
|