@ruvector/edge-net 0.4.6 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +13 -2
- package/plugins/cli.js +395 -0
- package/plugins/implementations/compression.js +132 -0
- package/plugins/implementations/e2e-encryption.js +160 -0
- package/plugins/implementations/federated-learning.js +249 -0
- package/plugins/implementations/reputation-staking.js +243 -0
- package/plugins/implementations/swarm-intelligence.js +386 -0
- package/plugins/index.js +90 -0
- package/plugins/plugin-loader.js +440 -0
- package/plugins/plugin-manifest.js +702 -0
- package/plugins/plugin-sdk.js +496 -0
- package/tests/plugin-system-test.js +382 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Plugin System Tests
|
|
4
|
+
*
|
|
5
|
+
* Comprehensive testing of the edge-net plugin architecture.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
PLUGIN_CATALOG,
|
|
10
|
+
PLUGIN_BUNDLES,
|
|
11
|
+
PluginCategory,
|
|
12
|
+
PluginTier,
|
|
13
|
+
Capability,
|
|
14
|
+
PluginLoader,
|
|
15
|
+
BasePlugin,
|
|
16
|
+
validateManifest,
|
|
17
|
+
validatePlugin,
|
|
18
|
+
PluginRegistry,
|
|
19
|
+
generatePluginTemplate,
|
|
20
|
+
} from '../plugins/index.js';
|
|
21
|
+
|
|
22
|
+
import { CompressionPlugin } from '../plugins/implementations/compression.js';
|
|
23
|
+
import { E2EEncryptionPlugin } from '../plugins/implementations/e2e-encryption.js';
|
|
24
|
+
import { FederatedLearningPlugin } from '../plugins/implementations/federated-learning.js';
|
|
25
|
+
import { ReputationStakingPlugin } from '../plugins/implementations/reputation-staking.js';
|
|
26
|
+
import { SwarmIntelligencePlugin } from '../plugins/implementations/swarm-intelligence.js';
|
|
27
|
+
|
|
28
|
+
// ============================================
|
|
29
|
+
// TEST UTILITIES
|
|
30
|
+
// ============================================
|
|
31
|
+
|
|
32
|
+
let passed = 0;
|
|
33
|
+
let failed = 0;
|
|
34
|
+
|
|
35
|
+
function test(name, fn) {
|
|
36
|
+
try {
|
|
37
|
+
fn();
|
|
38
|
+
console.log(`✅ ${name}`);
|
|
39
|
+
passed++;
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.log(`❌ ${name}`);
|
|
42
|
+
console.log(` Error: ${error.message}`);
|
|
43
|
+
failed++;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function testAsync(name, fn) {
|
|
48
|
+
try {
|
|
49
|
+
await fn();
|
|
50
|
+
console.log(`✅ ${name}`);
|
|
51
|
+
passed++;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.log(`❌ ${name}`);
|
|
54
|
+
console.log(` Error: ${error.message}`);
|
|
55
|
+
failed++;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function assert(condition, message) {
|
|
60
|
+
if (!condition) throw new Error(message || 'Assertion failed');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function assertEqual(actual, expected, message) {
|
|
64
|
+
if (actual !== expected) {
|
|
65
|
+
throw new Error(message || `Expected ${expected}, got ${actual}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ============================================
|
|
70
|
+
// TESTS
|
|
71
|
+
// ============================================
|
|
72
|
+
|
|
73
|
+
console.log('\n╔════════════════════════════════════════════════════════════════╗');
|
|
74
|
+
console.log('║ PLUGIN SYSTEM TESTS ║');
|
|
75
|
+
console.log('╚════════════════════════════════════════════════════════════════╝\n');
|
|
76
|
+
|
|
77
|
+
// --- Catalog Tests ---
|
|
78
|
+
console.log('\n--- Plugin Catalog ---\n');
|
|
79
|
+
|
|
80
|
+
test('Catalog has plugins', () => {
|
|
81
|
+
assert(Object.keys(PLUGIN_CATALOG).length > 0, 'Catalog should have plugins');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('All plugins have required fields', () => {
|
|
85
|
+
for (const [id, plugin] of Object.entries(PLUGIN_CATALOG)) {
|
|
86
|
+
assert(plugin.id === id, `Plugin ${id} ID mismatch`);
|
|
87
|
+
assert(plugin.name, `Plugin ${id} missing name`);
|
|
88
|
+
assert(plugin.version, `Plugin ${id} missing version`);
|
|
89
|
+
assert(plugin.description, `Plugin ${id} missing description`);
|
|
90
|
+
assert(plugin.category, `Plugin ${id} missing category`);
|
|
91
|
+
assert(plugin.tier, `Plugin ${id} missing tier`);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('Plugin categories are valid', () => {
|
|
96
|
+
const validCategories = Object.values(PluginCategory);
|
|
97
|
+
for (const [id, plugin] of Object.entries(PLUGIN_CATALOG)) {
|
|
98
|
+
assert(validCategories.includes(plugin.category),
|
|
99
|
+
`Plugin ${id} has invalid category: ${plugin.category}`);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('Plugin tiers are valid', () => {
|
|
104
|
+
const validTiers = Object.values(PluginTier);
|
|
105
|
+
for (const [id, plugin] of Object.entries(PLUGIN_CATALOG)) {
|
|
106
|
+
assert(validTiers.includes(plugin.tier),
|
|
107
|
+
`Plugin ${id} has invalid tier: ${plugin.tier}`);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('Bundles reference valid plugins', () => {
|
|
112
|
+
for (const [bundleId, bundle] of Object.entries(PLUGIN_BUNDLES)) {
|
|
113
|
+
for (const pluginId of bundle.plugins) {
|
|
114
|
+
assert(PLUGIN_CATALOG[pluginId],
|
|
115
|
+
`Bundle ${bundleId} references missing plugin: ${pluginId}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// --- Plugin Loader Tests ---
|
|
121
|
+
console.log('\n--- Plugin Loader ---\n');
|
|
122
|
+
|
|
123
|
+
test('Plugin loader initializes', () => {
|
|
124
|
+
const loader = new PluginLoader();
|
|
125
|
+
assert(loader.getCatalog().length > 0, 'Loader should see catalog');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('Loader respects tier restrictions', () => {
|
|
129
|
+
const loader = new PluginLoader({
|
|
130
|
+
allowedTiers: [PluginTier.STABLE],
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const catalog = loader.getCatalog();
|
|
134
|
+
const betaPlugin = catalog.find(p => p.tier === PluginTier.BETA);
|
|
135
|
+
|
|
136
|
+
if (betaPlugin) {
|
|
137
|
+
assert(!betaPlugin.isAllowed.allowed, 'Beta plugins should not be allowed');
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('Loader respects capability restrictions', () => {
|
|
142
|
+
const loader = new PluginLoader({
|
|
143
|
+
deniedCapabilities: [Capability.SYSTEM_EXEC],
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const catalog = loader.getCatalog();
|
|
147
|
+
const execPlugin = catalog.find(p =>
|
|
148
|
+
p.capabilities?.includes(Capability.SYSTEM_EXEC)
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
if (execPlugin) {
|
|
152
|
+
assert(!execPlugin.isAllowed.allowed, 'Exec plugins should not be allowed');
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// --- Manifest Validation Tests ---
|
|
157
|
+
console.log('\n--- Manifest Validation ---\n');
|
|
158
|
+
|
|
159
|
+
test('Valid manifest passes validation', () => {
|
|
160
|
+
const manifest = {
|
|
161
|
+
id: 'test.valid-plugin',
|
|
162
|
+
name: 'Valid Plugin',
|
|
163
|
+
version: '1.0.0',
|
|
164
|
+
description: 'A valid test plugin',
|
|
165
|
+
category: PluginCategory.CORE,
|
|
166
|
+
tier: PluginTier.STABLE,
|
|
167
|
+
capabilities: [Capability.COMPUTE_WASM],
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const result = validateManifest(manifest);
|
|
171
|
+
assert(result.valid, `Validation should pass: ${result.errors.join(', ')}`);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('Invalid ID fails validation', () => {
|
|
175
|
+
const manifest = {
|
|
176
|
+
id: 'InvalidID',
|
|
177
|
+
name: 'Test',
|
|
178
|
+
version: '1.0.0',
|
|
179
|
+
description: 'Test',
|
|
180
|
+
category: PluginCategory.CORE,
|
|
181
|
+
tier: PluginTier.STABLE,
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const result = validateManifest(manifest);
|
|
185
|
+
assert(!result.valid, 'Invalid ID should fail');
|
|
186
|
+
assert(result.errors.some(e => e.includes('ID')), 'Should mention ID error');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('Missing fields fail validation', () => {
|
|
190
|
+
const manifest = { id: 'test.plugin' };
|
|
191
|
+
const result = validateManifest(manifest);
|
|
192
|
+
assert(!result.valid, 'Missing fields should fail');
|
|
193
|
+
assert(result.errors.length > 0, 'Should have errors');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// --- Plugin SDK Tests ---
|
|
197
|
+
console.log('\n--- Plugin SDK ---\n');
|
|
198
|
+
|
|
199
|
+
test('BasePlugin can be extended', () => {
|
|
200
|
+
class TestPlugin extends BasePlugin {
|
|
201
|
+
static manifest = {
|
|
202
|
+
id: 'test.sdk-plugin',
|
|
203
|
+
name: 'SDK Test Plugin',
|
|
204
|
+
version: '1.0.0',
|
|
205
|
+
description: 'Testing SDK',
|
|
206
|
+
category: PluginCategory.CORE,
|
|
207
|
+
tier: PluginTier.EXPERIMENTAL,
|
|
208
|
+
capabilities: [],
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
doSomething() {
|
|
212
|
+
return 'worked';
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const plugin = new TestPlugin({ option: 'value' });
|
|
217
|
+
assertEqual(plugin.doSomething(), 'worked', 'Plugin method should work');
|
|
218
|
+
assertEqual(plugin.config.option, 'value', 'Config should be set');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('Plugin registry works', () => {
|
|
222
|
+
const registry = new PluginRegistry();
|
|
223
|
+
|
|
224
|
+
class TestPlugin extends BasePlugin {
|
|
225
|
+
static manifest = {
|
|
226
|
+
id: 'test.registry-plugin',
|
|
227
|
+
name: 'Registry Test',
|
|
228
|
+
version: '1.0.0',
|
|
229
|
+
description: 'Testing registry',
|
|
230
|
+
category: PluginCategory.CORE,
|
|
231
|
+
tier: PluginTier.EXPERIMENTAL,
|
|
232
|
+
capabilities: [],
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const result = registry.register(TestPlugin);
|
|
237
|
+
assert(result.id === 'test.registry-plugin', 'Should return ID');
|
|
238
|
+
assert(result.checksum, 'Should generate checksum');
|
|
239
|
+
assert(registry.has('test.registry-plugin'), 'Should be registered');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test('Template generator works', () => {
|
|
243
|
+
const template = generatePluginTemplate({
|
|
244
|
+
id: 'my-org.my-plugin',
|
|
245
|
+
name: 'My Plugin',
|
|
246
|
+
category: PluginCategory.AI,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
assert(template.includes('class'), 'Should generate class');
|
|
250
|
+
assert(template.includes('my-org.my-plugin'), 'Should include ID');
|
|
251
|
+
assert(template.includes('BasePlugin'), 'Should extend BasePlugin');
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// --- Implementation Tests ---
|
|
255
|
+
console.log('\n--- Plugin Implementations ---\n');
|
|
256
|
+
|
|
257
|
+
test('Compression plugin works', () => {
|
|
258
|
+
const comp = new CompressionPlugin({ threshold: 10 });
|
|
259
|
+
|
|
260
|
+
// Test with compressible data
|
|
261
|
+
const data = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
|
|
262
|
+
const result = comp.compress(data);
|
|
263
|
+
|
|
264
|
+
assert(result.compressed, 'Should compress');
|
|
265
|
+
assert(result.compressedSize < data.length, 'Should reduce size');
|
|
266
|
+
|
|
267
|
+
const decompressed = comp.decompress(result.data, true);
|
|
268
|
+
assertEqual(decompressed.toString(), data, 'Should decompress correctly');
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test('E2E Encryption plugin works', async () => {
|
|
272
|
+
const crypto = new E2EEncryptionPlugin();
|
|
273
|
+
|
|
274
|
+
// Establish session
|
|
275
|
+
await crypto.establishSession('peer-1', 'fake-public-key');
|
|
276
|
+
assert(crypto.hasSession('peer-1'), 'Session should exist');
|
|
277
|
+
|
|
278
|
+
// Encrypt/decrypt
|
|
279
|
+
const message = 'Hello, secure world!';
|
|
280
|
+
const encrypted = crypto.encrypt('peer-1', message);
|
|
281
|
+
|
|
282
|
+
assert(encrypted.ciphertext, 'Should have ciphertext');
|
|
283
|
+
assert(encrypted.iv, 'Should have IV');
|
|
284
|
+
assert(encrypted.authTag, 'Should have auth tag');
|
|
285
|
+
|
|
286
|
+
const decrypted = crypto.decrypt('peer-1', encrypted);
|
|
287
|
+
assertEqual(decrypted, message, 'Should decrypt correctly');
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test('Federated Learning plugin works', async () => {
|
|
291
|
+
const fl = new FederatedLearningPlugin({
|
|
292
|
+
minParticipants: 2,
|
|
293
|
+
localEpochs: 2,
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// Start round
|
|
297
|
+
const globalWeights = [0, 0, 0, 0, 0];
|
|
298
|
+
const roundId = fl.startRound('test-model', globalWeights);
|
|
299
|
+
|
|
300
|
+
assert(roundId, 'Should return round ID');
|
|
301
|
+
|
|
302
|
+
// Simulate local training
|
|
303
|
+
const localData = [
|
|
304
|
+
{ features: [1, 2, 3, 4, 5] },
|
|
305
|
+
{ features: [2, 3, 4, 5, 6] },
|
|
306
|
+
];
|
|
307
|
+
|
|
308
|
+
await fl.trainLocal(roundId, localData, { participantId: 'node-1' });
|
|
309
|
+
await fl.trainLocal(roundId, localData, { participantId: 'node-2' });
|
|
310
|
+
|
|
311
|
+
// Check aggregation happened
|
|
312
|
+
const status = fl.getRoundStatus(roundId);
|
|
313
|
+
assertEqual(status.status, 'completed', 'Round should complete');
|
|
314
|
+
assertEqual(status.participants, 2, 'Should have 2 participants');
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test('Reputation staking plugin works', () => {
|
|
318
|
+
const staking = new ReputationStakingPlugin({ minStake: 5 });
|
|
319
|
+
|
|
320
|
+
// Mock credit system
|
|
321
|
+
const credits = {
|
|
322
|
+
balance: 100,
|
|
323
|
+
getBalance: () => credits.balance,
|
|
324
|
+
spendCredits: (_, amount) => { credits.balance -= amount; },
|
|
325
|
+
earnCredits: (_, amount) => { credits.balance += amount; },
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
// Stake
|
|
329
|
+
const stake = staking.stake('node-1', 20, credits);
|
|
330
|
+
assertEqual(stake.staked, 20, 'Should stake 20');
|
|
331
|
+
assertEqual(stake.reputation, 100, 'Should start at 100 rep');
|
|
332
|
+
|
|
333
|
+
// Record success
|
|
334
|
+
staking.recordSuccess('node-1');
|
|
335
|
+
const newStake = staking.getStake('node-1');
|
|
336
|
+
assertEqual(newStake.successfulTasks, 1, 'Should record success');
|
|
337
|
+
|
|
338
|
+
// Slash
|
|
339
|
+
const slashResult = staking.slash('node-1', 'test-misbehavior', 0.5);
|
|
340
|
+
assert(slashResult.slashed > 0, 'Should slash');
|
|
341
|
+
assert(slashResult.newReputation < 100, 'Should reduce reputation');
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test('Swarm intelligence plugin works', async () => {
|
|
345
|
+
const swarm = new SwarmIntelligencePlugin({
|
|
346
|
+
populationSize: 20,
|
|
347
|
+
iterations: 50,
|
|
348
|
+
dimensions: 5,
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// Create swarm with sphere function (minimize x²)
|
|
352
|
+
swarm.createSwarm('test-swarm', {
|
|
353
|
+
algorithm: 'pso',
|
|
354
|
+
bounds: { min: -10, max: 10 },
|
|
355
|
+
fitnessFunction: (x) => x.reduce((sum, v) => sum + v * v, 0),
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// Run optimization
|
|
359
|
+
const result = await swarm.optimize('test-swarm', {
|
|
360
|
+
iterations: 50,
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
assert(result.bestFitness < 1, 'Should find good solution');
|
|
364
|
+
assert(result.iterations === 50, 'Should run 50 iterations');
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// --- Summary ---
|
|
368
|
+
console.log('\n╔════════════════════════════════════════════════════════════════╗');
|
|
369
|
+
console.log('║ TEST SUMMARY ║');
|
|
370
|
+
console.log('╚════════════════════════════════════════════════════════════════╝\n');
|
|
371
|
+
|
|
372
|
+
console.log(` Passed: ${passed}`);
|
|
373
|
+
console.log(` Failed: ${failed}`);
|
|
374
|
+
console.log(` Total: ${passed + failed}\n`);
|
|
375
|
+
|
|
376
|
+
if (failed > 0) {
|
|
377
|
+
console.log('❌ Some tests failed\n');
|
|
378
|
+
process.exit(1);
|
|
379
|
+
} else {
|
|
380
|
+
console.log('✅ All tests passed!\n');
|
|
381
|
+
process.exit(0);
|
|
382
|
+
}
|