@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.
@@ -0,0 +1,534 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Task Execution Handler Integration Test
4
+ *
5
+ * Tests the distributed task execution system:
6
+ * - TaskExecutionHandler receives task-assign signals
7
+ * - Validates tasks and executes via RealWorkerPool
8
+ * - Sends task-result or task-error back to originator
9
+ *
10
+ * Run: node tests/task-execution-test.js
11
+ */
12
+
13
+ import { EventEmitter } from 'events';
14
+ import { randomBytes } from 'crypto';
15
+
16
+ // Import components
17
+ import { TaskExecutionHandler, TaskValidator, DistributedTaskNetwork } from '../task-execution-handler.js';
18
+ import { RealWorkerPool } from '../real-workers.js';
19
+
20
+ // ============================================
21
+ // MOCK SIGNALING (for testing without Firebase)
22
+ // ============================================
23
+
24
+ class MockSignaling extends EventEmitter {
25
+ constructor(peerId) {
26
+ super();
27
+ this.peerId = peerId || `mock-${randomBytes(8).toString('hex')}`;
28
+ this.isConnected = true;
29
+ this.peers = new Map();
30
+ this.sentSignals = [];
31
+ }
32
+
33
+ async sendSignal(toPeerId, type, data) {
34
+ const signal = {
35
+ from: this.peerId,
36
+ to: toPeerId,
37
+ type,
38
+ data,
39
+ timestamp: Date.now(),
40
+ };
41
+ this.sentSignals.push(signal);
42
+ return true;
43
+ }
44
+
45
+ getOnlinePeers() {
46
+ return Array.from(this.peers.values());
47
+ }
48
+
49
+ // Simulate receiving a signal
50
+ simulateSignal(signal) {
51
+ this.emit('signal', signal);
52
+ }
53
+
54
+ async connect() {
55
+ this.isConnected = true;
56
+ return true;
57
+ }
58
+
59
+ async disconnect() {
60
+ this.isConnected = false;
61
+ }
62
+ }
63
+
64
+ // ============================================
65
+ // TEST UTILITIES
66
+ // ============================================
67
+
68
+ function log(msg, level = 'info') {
69
+ const timestamp = new Date().toISOString().slice(11, 23);
70
+ const prefix = {
71
+ info: '\x1b[36m[INFO]\x1b[0m',
72
+ pass: '\x1b[32m[PASS]\x1b[0m',
73
+ fail: '\x1b[31m[FAIL]\x1b[0m',
74
+ warn: '\x1b[33m[WARN]\x1b[0m',
75
+ }[level] || '[INFO]';
76
+ console.log(`${timestamp} ${prefix} ${msg}`);
77
+ }
78
+
79
+ async function runTest(name, testFn) {
80
+ log(`Running: ${name}`);
81
+ try {
82
+ await testFn();
83
+ log(`${name}`, 'pass');
84
+ return true;
85
+ } catch (error) {
86
+ log(`${name}: ${error.message}`, 'fail');
87
+ console.error(error);
88
+ return false;
89
+ }
90
+ }
91
+
92
+ // ============================================
93
+ // TESTS
94
+ // ============================================
95
+
96
+ async function testTaskValidator() {
97
+ const validator = new TaskValidator();
98
+
99
+ // Valid task
100
+ const validTask = {
101
+ id: 'task-123',
102
+ type: 'compute',
103
+ data: [1, 2, 3, 4, 5],
104
+ priority: 'medium',
105
+ };
106
+
107
+ const result1 = validator.validate(validTask);
108
+ if (!result1.valid) throw new Error(`Valid task rejected: ${result1.errors.join(', ')}`);
109
+
110
+ // Missing ID
111
+ const result2 = validator.validate({ type: 'compute', data: [] });
112
+ if (result2.valid) throw new Error('Task without ID should be rejected');
113
+
114
+ // Missing type
115
+ const result3 = validator.validate({ id: 'test', data: [] });
116
+ if (result3.valid) throw new Error('Task without type should be rejected');
117
+
118
+ // Invalid type
119
+ const result4 = validator.validate({ id: 'test', type: 'invalid-type', data: [] });
120
+ if (result4.valid) throw new Error('Task with invalid type should be rejected');
121
+
122
+ // Missing data
123
+ const result5 = validator.validate({ id: 'test', type: 'compute' });
124
+ if (result5.valid) throw new Error('Task without data should be rejected');
125
+
126
+ log('TaskValidator tests passed');
127
+ }
128
+
129
+ async function testWorkerPoolExecution() {
130
+ const pool = new RealWorkerPool({ size: 2 });
131
+ await pool.initialize();
132
+
133
+ // Test compute task
134
+ const computeResult = await pool.execute('compute', [1, 2, 3, 4, 5], { operation: 'sum' });
135
+ if (computeResult.result !== 15) {
136
+ throw new Error(`Expected sum to be 15, got ${computeResult.result}`);
137
+ }
138
+
139
+ // Test embed task
140
+ const embedResult = await pool.execute('embed', 'hello world');
141
+ if (!embedResult.embedding || embedResult.embedding.length !== 384) {
142
+ throw new Error('Embed result should have 384-dimension embedding');
143
+ }
144
+
145
+ // Test transform task
146
+ const transformResult = await pool.execute('transform', 'hello', { transform: 'uppercase' });
147
+ if (transformResult.transformed !== 'HELLO') {
148
+ throw new Error(`Expected HELLO, got ${transformResult.transformed}`);
149
+ }
150
+
151
+ await pool.shutdown();
152
+ log('WorkerPool execution tests passed');
153
+ }
154
+
155
+ async function testTaskExecutionHandler() {
156
+ // Create mock signaling and real worker pool
157
+ const nodeASignaling = new MockSignaling('node-A');
158
+ const nodeBSignaling = new MockSignaling('node-B');
159
+
160
+ const workerPool = new RealWorkerPool({ size: 2 });
161
+ await workerPool.initialize();
162
+
163
+ // Create handler for Node B (the executor)
164
+ const handler = new TaskExecutionHandler({
165
+ signaling: nodeBSignaling,
166
+ workerPool,
167
+ nodeId: 'node-B',
168
+ capabilities: ['compute', 'embed', 'process'],
169
+ });
170
+ handler.attach();
171
+
172
+ // Track events with promise-based waiting
173
+ const taskId = `task-${Date.now()}`;
174
+ let startEvent = null;
175
+ let completeEvent = null;
176
+
177
+ const completePromise = new Promise((resolve) => {
178
+ handler.on('task-start', (e) => {
179
+ if (e.taskId === taskId) startEvent = e;
180
+ });
181
+ handler.on('task-complete', (e) => {
182
+ if (e.taskId === taskId) {
183
+ completeEvent = e;
184
+ resolve(e);
185
+ }
186
+ });
187
+ handler.on('task-error', (e) => {
188
+ if (e.taskId === taskId) resolve(e);
189
+ });
190
+ });
191
+
192
+ // Simulate Node A sending a task-assign signal to Node B
193
+ const taskAssignSignal = {
194
+ type: 'task-assign',
195
+ from: 'node-A',
196
+ data: {
197
+ task: {
198
+ id: taskId,
199
+ type: 'compute',
200
+ data: [10, 20, 30],
201
+ options: { operation: 'sum' },
202
+ },
203
+ },
204
+ verified: true,
205
+ };
206
+
207
+ // Send signal to Node B's handler
208
+ nodeBSignaling.simulateSignal(taskAssignSignal);
209
+
210
+ // Wait for completion (with timeout)
211
+ await Promise.race([
212
+ completePromise,
213
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout waiting for task completion')), 5000))
214
+ ]);
215
+
216
+ // Check events
217
+ if (!startEvent) throw new Error('task-start event not emitted');
218
+ if (!completeEvent) throw new Error('task-complete event not emitted');
219
+
220
+ // Check that result was sent back via signaling
221
+ const resultSignal = nodeBSignaling.sentSignals.find(
222
+ s => s.type === 'task-result' && s.data.taskId === taskId
223
+ );
224
+ if (!resultSignal) throw new Error('task-result signal not sent');
225
+
226
+ if (resultSignal.data.result.result !== 60) {
227
+ throw new Error(`Expected sum result 60, got ${resultSignal.data.result.result}`);
228
+ }
229
+
230
+ // Cleanup
231
+ handler.detach();
232
+ await workerPool.shutdown();
233
+
234
+ log('TaskExecutionHandler tests passed');
235
+ }
236
+
237
+ async function testTaskRejection() {
238
+ const signaling = new MockSignaling('node-B');
239
+ const workerPool = new RealWorkerPool({ size: 1 });
240
+ await workerPool.initialize();
241
+
242
+ const handler = new TaskExecutionHandler({
243
+ signaling,
244
+ workerPool,
245
+ nodeId: 'node-B',
246
+ capabilities: ['compute'],
247
+ });
248
+ handler.attach();
249
+
250
+ const events = [];
251
+ handler.on('task-rejected', (e) => events.push(e));
252
+
253
+ // Test 1: Invalid task (missing type)
254
+ signaling.simulateSignal({
255
+ type: 'task-assign',
256
+ from: 'node-A',
257
+ data: {
258
+ task: {
259
+ id: 'bad-task-1',
260
+ data: [1, 2, 3],
261
+ // missing type
262
+ },
263
+ },
264
+ verified: true,
265
+ });
266
+
267
+ await new Promise(resolve => setTimeout(resolve, 100));
268
+
269
+ const rejection1 = events.find(e => e.taskId === 'bad-task-1');
270
+ if (!rejection1) throw new Error('Task without type should be rejected');
271
+
272
+ // Check error signal was sent
273
+ const errorSignal = signaling.sentSignals.find(
274
+ s => s.type === 'task-error' && s.data.taskId === 'bad-task-1'
275
+ );
276
+ if (!errorSignal) throw new Error('task-error signal should be sent for invalid task');
277
+
278
+ // Test 2: Missing capabilities
279
+ signaling.simulateSignal({
280
+ type: 'task-assign',
281
+ from: 'node-A',
282
+ data: {
283
+ task: {
284
+ id: 'bad-task-2',
285
+ type: 'compute',
286
+ data: [1, 2, 3],
287
+ requiredCapabilities: ['special-gpu'],
288
+ },
289
+ },
290
+ verified: true,
291
+ });
292
+
293
+ await new Promise(resolve => setTimeout(resolve, 100));
294
+
295
+ const rejection2 = events.find(e => e.taskId === 'bad-task-2');
296
+ if (!rejection2 || rejection2.reason !== 'capabilities') {
297
+ throw new Error('Task with missing capabilities should be rejected');
298
+ }
299
+
300
+ handler.detach();
301
+ await workerPool.shutdown();
302
+
303
+ log('Task rejection tests passed');
304
+ }
305
+
306
+ async function testSubmitTaskToRemote() {
307
+ // Create two nodes that can communicate
308
+ const nodeASignaling = new MockSignaling('node-A');
309
+ const nodeBSignaling = new MockSignaling('node-B');
310
+
311
+ // Register each other as peers
312
+ nodeASignaling.peers.set('node-B', { id: 'node-B', capabilities: ['compute'] });
313
+ nodeBSignaling.peers.set('node-A', { id: 'node-A', capabilities: ['compute'] });
314
+
315
+ // Create worker pool and handler for Node B
316
+ const workerPool = new RealWorkerPool({ size: 2 });
317
+ await workerPool.initialize();
318
+
319
+ const nodeBHandler = new TaskExecutionHandler({
320
+ signaling: nodeBSignaling,
321
+ workerPool,
322
+ nodeId: 'node-B',
323
+ capabilities: ['compute'],
324
+ });
325
+ nodeBHandler.attach();
326
+
327
+ // Create handler for Node A (the submitter)
328
+ const nodeAHandler = new TaskExecutionHandler({
329
+ signaling: nodeASignaling,
330
+ workerPool: null, // Node A won't execute locally
331
+ nodeId: 'node-A',
332
+ capabilities: [],
333
+ });
334
+ nodeAHandler.attach();
335
+
336
+ // Override sendSignal for Node A to deliver to Node B
337
+ const origSendA = nodeASignaling.sendSignal.bind(nodeASignaling);
338
+ nodeASignaling.sendSignal = async (to, type, data) => {
339
+ await origSendA(to, type, data);
340
+ if (to === 'node-B') {
341
+ // Small delay to simulate network
342
+ await new Promise(r => setTimeout(r, 10));
343
+ // Deliver to Node B with proper signal structure
344
+ nodeBSignaling.simulateSignal({
345
+ type,
346
+ from: 'node-A',
347
+ data,
348
+ verified: true,
349
+ });
350
+ }
351
+ };
352
+
353
+ // Override sendSignal for Node B to deliver results back to Node A
354
+ const origSendB = nodeBSignaling.sendSignal.bind(nodeBSignaling);
355
+ nodeBSignaling.sendSignal = async (to, type, data) => {
356
+ await origSendB(to, type, data);
357
+ if (to === 'node-A' && (type === 'task-result' || type === 'task-error')) {
358
+ // Small delay to simulate network
359
+ await new Promise(r => setTimeout(r, 10));
360
+ // Deliver result back to Node A
361
+ nodeASignaling.simulateSignal({
362
+ type,
363
+ from: 'node-B',
364
+ data,
365
+ verified: true,
366
+ });
367
+ }
368
+ };
369
+
370
+ // Submit task from Node A to Node B
371
+ const resultPromise = nodeAHandler.submitTask('node-B', {
372
+ id: 'remote-task-1',
373
+ type: 'compute',
374
+ data: [5, 10, 15],
375
+ options: { operation: 'sum' },
376
+ }, { timeout: 5000 });
377
+
378
+ // Wait for result
379
+ const result = await resultPromise;
380
+
381
+ if (result.result.result !== 30) {
382
+ throw new Error(`Expected 30, got ${result.result.result}`);
383
+ }
384
+
385
+ if (result.processedBy !== 'node-B') {
386
+ throw new Error(`Expected processedBy to be node-B, got ${result.processedBy}`);
387
+ }
388
+
389
+ // Cleanup
390
+ nodeAHandler.detach();
391
+ nodeBHandler.detach();
392
+ await workerPool.shutdown();
393
+
394
+ log('Remote task submission tests passed');
395
+ }
396
+
397
+ async function testCapacityLimit() {
398
+ const signaling = new MockSignaling('node-B');
399
+ const workerPool = new RealWorkerPool({ size: 1 });
400
+ await workerPool.initialize();
401
+
402
+ const handler = new TaskExecutionHandler({
403
+ signaling,
404
+ workerPool,
405
+ nodeId: 'node-B',
406
+ maxConcurrentTasks: 2, // Low limit for testing
407
+ capabilities: ['compute'],
408
+ });
409
+ handler.attach();
410
+
411
+ const events = [];
412
+ handler.on('task-rejected', (e) => events.push(e));
413
+ handler.on('task-start', (e) => events.push({ type: 'start', ...e }));
414
+
415
+ // Submit 3 tasks rapidly
416
+ for (let i = 0; i < 3; i++) {
417
+ signaling.simulateSignal({
418
+ type: 'task-assign',
419
+ from: 'node-A',
420
+ data: {
421
+ task: {
422
+ id: `capacity-task-${i}`,
423
+ type: 'compute',
424
+ data: Array(1000).fill(1), // Larger task to take time
425
+ options: { operation: 'sum' },
426
+ },
427
+ },
428
+ verified: true,
429
+ });
430
+ }
431
+
432
+ // Wait a bit
433
+ await new Promise(resolve => setTimeout(resolve, 200));
434
+
435
+ // Check that we got at least one rejection for capacity
436
+ const capacityRejection = events.find(e => e.reason === 'capacity');
437
+ if (!capacityRejection) {
438
+ log('Note: Capacity test depends on timing, may not always trigger', 'warn');
439
+ }
440
+
441
+ handler.detach();
442
+ await workerPool.shutdown();
443
+
444
+ log('Capacity limit tests completed');
445
+ }
446
+
447
+ async function testProgressReporting() {
448
+ const signaling = new MockSignaling('node-B');
449
+ const workerPool = new RealWorkerPool({ size: 1 });
450
+ await workerPool.initialize();
451
+
452
+ const handler = new TaskExecutionHandler({
453
+ signaling,
454
+ workerPool,
455
+ nodeId: 'node-B',
456
+ capabilities: ['compute'],
457
+ reportProgress: true,
458
+ progressInterval: 100, // Fast progress for testing
459
+ });
460
+ handler.attach();
461
+
462
+ // Submit a task that takes some time
463
+ signaling.simulateSignal({
464
+ type: 'task-assign',
465
+ from: 'node-A',
466
+ data: {
467
+ task: {
468
+ id: 'progress-task',
469
+ type: 'compute',
470
+ data: Array(10000).fill(1),
471
+ options: { operation: 'sum' },
472
+ },
473
+ },
474
+ verified: true,
475
+ });
476
+
477
+ // Wait for completion
478
+ await new Promise(resolve => setTimeout(resolve, 500));
479
+
480
+ // Check for progress signals
481
+ const progressSignals = signaling.sentSignals.filter(s => s.type === 'task-progress');
482
+ // Progress reporting may or may not fire depending on execution speed
483
+ log(`Progress signals sent: ${progressSignals.length}`, 'info');
484
+
485
+ // Check for result signal
486
+ const resultSignal = signaling.sentSignals.find(s => s.type === 'task-result');
487
+ if (!resultSignal) throw new Error('Result signal not sent');
488
+
489
+ handler.detach();
490
+ await workerPool.shutdown();
491
+
492
+ log('Progress reporting tests passed');
493
+ }
494
+
495
+ // ============================================
496
+ // MAIN
497
+ // ============================================
498
+
499
+ async function main() {
500
+ console.log('\n========================================');
501
+ console.log(' Task Execution Handler Integration Tests');
502
+ console.log('========================================\n');
503
+
504
+ const tests = [
505
+ ['TaskValidator', testTaskValidator],
506
+ ['WorkerPool Execution', testWorkerPoolExecution],
507
+ ['TaskExecutionHandler Basic', testTaskExecutionHandler],
508
+ ['Task Rejection', testTaskRejection],
509
+ ['Remote Task Submission', testSubmitTaskToRemote],
510
+ ['Capacity Limits', testCapacityLimit],
511
+ ['Progress Reporting', testProgressReporting],
512
+ ];
513
+
514
+ let passed = 0;
515
+ let failed = 0;
516
+
517
+ for (const [name, testFn] of tests) {
518
+ console.log('');
519
+ const success = await runTest(name, testFn);
520
+ if (success) passed++;
521
+ else failed++;
522
+ }
523
+
524
+ console.log('\n========================================');
525
+ console.log(` Results: ${passed} passed, ${failed} failed`);
526
+ console.log('========================================\n');
527
+
528
+ process.exit(failed > 0 ? 1 : 0);
529
+ }
530
+
531
+ main().catch(err => {
532
+ console.error('Test runner error:', err);
533
+ process.exit(1);
534
+ });