480 lines
17 KiB
JavaScript
Executable File
480 lines
17 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
/**
|
|
* Edge-Net Contributor Flow Validation
|
|
*
|
|
* Tests the complete CONTRIBUTOR FLOW:
|
|
* 1. Identity creation/restoration
|
|
* 2. Contribution tracking (local + QDAG)
|
|
* 3. Credit earning and persistence
|
|
* 4. WebSocket relay communication
|
|
* 5. Dashboard data flow
|
|
* 6. Multi-device sync capability
|
|
*/
|
|
|
|
const { promises: fs } = require('fs');
|
|
const { homedir } = require('os');
|
|
const { join } = require('path');
|
|
const WebSocket = require('ws');
|
|
|
|
// ANSI colors for output
|
|
const colors = {
|
|
reset: '\x1b[0m',
|
|
bold: '\x1b[1m',
|
|
green: '\x1b[32m',
|
|
red: '\x1b[31m',
|
|
yellow: '\x1b[33m',
|
|
cyan: '\x1b[36m',
|
|
dim: '\x1b[2m',
|
|
};
|
|
|
|
const c = (color, text) => `${colors[color]}${text}${colors.reset}`;
|
|
|
|
// Configuration
|
|
const CONFIG = {
|
|
relayUrl: 'wss://edge-net-relay-875130704813.us-central1.run.app',
|
|
dashboardUrl: 'https://edge-net-dashboard-875130704813.us-central1.run.app',
|
|
identityPath: join(homedir(), '.ruvector', 'identities', 'edge-contributor.meta.json'),
|
|
qdagPath: join(homedir(), '.ruvector', 'network', 'qdag.json'),
|
|
historyPath: join(homedir(), '.ruvector', 'contributions', 'edge-contributor.history.json'),
|
|
};
|
|
|
|
class ContributorFlowValidator {
|
|
constructor() {
|
|
this.results = {
|
|
passed: [],
|
|
failed: [],
|
|
warnings: [],
|
|
};
|
|
}
|
|
|
|
async run() {
|
|
console.log(`\n${c('bold', '═══════════════════════════════════════════════════')}`);
|
|
console.log(c('cyan', ' Edge-Net CONTRIBUTOR FLOW Validation'));
|
|
console.log(`${c('bold', '═══════════════════════════════════════════════════')}\n`);
|
|
|
|
await this.testIdentityPersistence();
|
|
await this.testContributionTracking();
|
|
await this.testQDAGPersistence();
|
|
await this.testCreditConsistency();
|
|
await this.testRelayConnection();
|
|
await this.testCreditEarningFlow();
|
|
await this.testDashboardAccess();
|
|
await this.testMultiDeviceSync();
|
|
|
|
this.printResults();
|
|
}
|
|
|
|
async testIdentityPersistence() {
|
|
console.log(`${c('bold', '1. Testing Identity Persistence...')}`);
|
|
|
|
try {
|
|
const exists = await fs.access(CONFIG.identityPath).then(() => true).catch(() => false);
|
|
|
|
if (!exists) {
|
|
this.fail('Identity file not found. Run: node join.js --generate');
|
|
return;
|
|
}
|
|
|
|
const meta = JSON.parse(await fs.readFile(CONFIG.identityPath, 'utf-8'));
|
|
|
|
// Validate identity structure
|
|
if (!meta.shortId || !meta.publicKey || !meta.genesisFingerprint) {
|
|
this.fail('Identity structure invalid');
|
|
return;
|
|
}
|
|
|
|
if (!meta.shortId.startsWith('π:')) {
|
|
this.fail('Invalid Pi-Key format');
|
|
return;
|
|
}
|
|
|
|
console.log(` ${c('green', '✓')} Identity loaded: ${meta.shortId}`);
|
|
console.log(` ${c('green', '✓')} Member since: ${new Date(meta.createdAt).toLocaleDateString()}`);
|
|
console.log(` ${c('green', '✓')} Total sessions: ${meta.totalSessions}`);
|
|
|
|
this.pass('Identity Persistence', {
|
|
shortId: meta.shortId,
|
|
sessions: meta.totalSessions,
|
|
contributions: meta.totalContributions,
|
|
});
|
|
} catch (err) {
|
|
this.fail('Identity Persistence', err.message);
|
|
}
|
|
}
|
|
|
|
async testContributionTracking() {
|
|
console.log(`\n${c('bold', '2. Testing Contribution Tracking...')}`);
|
|
|
|
try {
|
|
const exists = await fs.access(CONFIG.historyPath).then(() => true).catch(() => false);
|
|
|
|
if (!exists) {
|
|
this.warn('No contribution history yet. Run: node join.js');
|
|
return;
|
|
}
|
|
|
|
const history = JSON.parse(await fs.readFile(CONFIG.historyPath, 'utf-8'));
|
|
|
|
console.log(` ${c('green', '✓')} Sessions tracked: ${history.sessions.length}`);
|
|
console.log(` ${c('green', '✓')} Contributions recorded: ${history.contributions.length}`);
|
|
console.log(` ${c('green', '✓')} Milestones: ${history.milestones.length}`);
|
|
|
|
// Validate contribution structure
|
|
if (history.contributions.length > 0) {
|
|
const lastContrib = history.contributions[history.contributions.length - 1];
|
|
if (!lastContrib.computeUnits || !lastContrib.credits) {
|
|
this.fail('Invalid contribution structure');
|
|
return;
|
|
}
|
|
|
|
console.log(` ${c('dim', 'Last contribution:')} ${lastContrib.computeUnits} compute units = ${lastContrib.credits} credits`);
|
|
}
|
|
|
|
this.pass('Contribution Tracking', {
|
|
sessions: history.sessions.length,
|
|
contributions: history.contributions.length,
|
|
});
|
|
} catch (err) {
|
|
this.fail('Contribution Tracking', err.message);
|
|
}
|
|
}
|
|
|
|
async testQDAGPersistence() {
|
|
console.log(`\n${c('bold', '3. Testing QDAG Persistence...')}`);
|
|
|
|
try {
|
|
const exists = await fs.access(CONFIG.qdagPath).then(() => true).catch(() => false);
|
|
|
|
if (!exists) {
|
|
this.warn('QDAG not initialized. Start contributing: node join.js');
|
|
return;
|
|
}
|
|
|
|
const qdag = JSON.parse(await fs.readFile(CONFIG.qdagPath, 'utf-8'));
|
|
|
|
console.log(` ${c('green', '✓')} QDAG nodes: ${qdag.nodes.length}`);
|
|
console.log(` ${c('green', '✓')} Confirmed: ${qdag.confirmed.length}`);
|
|
console.log(` ${c('green', '✓')} Tips: ${qdag.tips.length}`);
|
|
|
|
// Validate QDAG structure (genesis is optional, savedAt is metadata)
|
|
if (!qdag.nodes || !qdag.confirmed || !qdag.tips) {
|
|
this.fail('Invalid QDAG structure');
|
|
return;
|
|
}
|
|
|
|
// Count contributions
|
|
const contributions = qdag.nodes.filter(n => n.type === 'contribution');
|
|
const totalCredits = contributions.reduce((sum, c) => sum + (c.credits || 0), 0);
|
|
|
|
console.log(` ${c('green', '✓')} Total contributions: ${contributions.length}`);
|
|
console.log(` ${c('green', '✓')} Total credits in ledger: ${totalCredits}`);
|
|
|
|
this.pass('QDAG Persistence', {
|
|
nodes: qdag.nodes.length,
|
|
contributions: contributions.length,
|
|
credits: totalCredits,
|
|
});
|
|
} catch (err) {
|
|
this.fail('QDAG Persistence', err.message);
|
|
}
|
|
}
|
|
|
|
async testCreditConsistency() {
|
|
console.log(`\n${c('bold', '4. Testing Credit Consistency...')}`);
|
|
|
|
try {
|
|
const meta = JSON.parse(await fs.readFile(CONFIG.identityPath, 'utf-8'));
|
|
const qdag = JSON.parse(await fs.readFile(CONFIG.qdagPath, 'utf-8'));
|
|
const history = JSON.parse(await fs.readFile(CONFIG.historyPath, 'utf-8'));
|
|
|
|
// Count credits from different sources
|
|
const metaContributions = meta.totalContributions;
|
|
const historyContributions = history.contributions.length;
|
|
const qdagContributions = qdag.nodes.filter(n =>
|
|
n.type === 'contribution' && n.contributor === meta.shortId
|
|
).length;
|
|
|
|
const historyCredits = history.contributions.reduce((sum, c) => sum + (c.credits || 0), 0);
|
|
const qdagCredits = qdag.nodes
|
|
.filter(n => n.type === 'contribution' && n.contributor === meta.shortId)
|
|
.reduce((sum, c) => sum + (c.credits || 0), 0);
|
|
|
|
console.log(` ${c('cyan', 'Meta contributions:')} ${metaContributions}`);
|
|
console.log(` ${c('cyan', 'History contributions:')} ${historyContributions}`);
|
|
console.log(` ${c('cyan', 'QDAG contributions:')} ${qdagContributions}`);
|
|
console.log(` ${c('cyan', 'History credits:')} ${historyCredits}`);
|
|
console.log(` ${c('cyan', 'QDAG credits:')} ${qdagCredits}`);
|
|
|
|
// Verify consistency
|
|
if (metaContributions !== historyContributions) {
|
|
this.warn(`Meta/History mismatch: ${metaContributions} vs ${historyContributions}`);
|
|
}
|
|
|
|
if (historyCredits !== qdagCredits) {
|
|
this.warn(`History/QDAG credit mismatch: ${historyCredits} vs ${qdagCredits}`);
|
|
}
|
|
|
|
if (metaContributions === historyContributions && historyCredits === qdagCredits) {
|
|
console.log(` ${c('green', '✓')} Perfect consistency across all storage layers`);
|
|
this.pass('Credit Consistency', { credits: qdagCredits });
|
|
} else {
|
|
console.log(` ${c('yellow', '⚠')} Minor inconsistency (expected during active contribution)`);
|
|
this.pass('Credit Consistency (with warnings)', { credits: qdagCredits });
|
|
}
|
|
} catch (err) {
|
|
this.fail('Credit Consistency', err.message);
|
|
}
|
|
}
|
|
|
|
async testRelayConnection() {
|
|
console.log(`\n${c('bold', '5. Testing Relay Connection...')}`);
|
|
|
|
return new Promise((resolve) => {
|
|
const ws = new WebSocket(CONFIG.relayUrl);
|
|
let connected = false;
|
|
|
|
const timeout = setTimeout(() => {
|
|
if (!connected) {
|
|
ws.close();
|
|
this.fail('Relay Connection', 'Connection timeout');
|
|
resolve();
|
|
}
|
|
}, 10000);
|
|
|
|
ws.on('open', () => {
|
|
connected = true;
|
|
console.log(` ${c('green', '✓')} WebSocket connected to relay`);
|
|
|
|
// Send registration
|
|
ws.send(JSON.stringify({
|
|
type: 'register',
|
|
contributor: 'validation-test',
|
|
capabilities: { cpu: 4 }
|
|
}));
|
|
});
|
|
|
|
ws.on('message', (data) => {
|
|
const msg = JSON.parse(data.toString());
|
|
|
|
if (msg.type === 'welcome') {
|
|
console.log(` ${c('green', '✓')} Received welcome message`);
|
|
console.log(` ${c('dim', 'Network state:')} ${msg.networkState.totalNodes} nodes, ${msg.networkState.activeNodes} active`);
|
|
}
|
|
|
|
if (msg.type === 'node_joined') {
|
|
console.log(` ${c('green', '✓')} Node registered in network`);
|
|
}
|
|
|
|
if (msg.type === 'time_crystal_sync') {
|
|
console.log(` ${c('green', '✓')} Time crystal sync received (phase: ${msg.phase.toFixed(2)})`);
|
|
|
|
clearTimeout(timeout);
|
|
ws.close();
|
|
this.pass('Relay Connection', { url: CONFIG.relayUrl });
|
|
resolve();
|
|
}
|
|
});
|
|
|
|
ws.on('error', (err) => {
|
|
clearTimeout(timeout);
|
|
this.fail('Relay Connection', err.message);
|
|
resolve();
|
|
});
|
|
});
|
|
}
|
|
|
|
async testCreditEarningFlow() {
|
|
console.log(`\n${c('bold', '6. Testing Credit Earning Flow...')}`);
|
|
|
|
return new Promise((resolve) => {
|
|
const ws = new WebSocket(CONFIG.relayUrl);
|
|
let registered = false;
|
|
|
|
const timeout = setTimeout(() => {
|
|
ws.close();
|
|
this.fail('Credit Earning Flow', 'Timeout waiting for credit confirmation');
|
|
resolve();
|
|
}, 15000);
|
|
|
|
ws.on('open', () => {
|
|
ws.send(JSON.stringify({
|
|
type: 'register',
|
|
contributor: 'credit-test-validator',
|
|
capabilities: { cpu: 8, memory: 16384 }
|
|
}));
|
|
|
|
console.log(` ${c('cyan', '→')} Sent registration`);
|
|
});
|
|
|
|
ws.on('message', (data) => {
|
|
const msg = JSON.parse(data.toString());
|
|
|
|
if (msg.type === 'welcome' && !registered) {
|
|
registered = true;
|
|
|
|
// Send credit_earned message
|
|
setTimeout(() => {
|
|
ws.send(JSON.stringify({
|
|
type: 'credit_earned',
|
|
contributor: 'credit-test-validator',
|
|
taskId: 'validation-task-' + Date.now(),
|
|
creditsEarned: 5,
|
|
computeUnits: 500,
|
|
timestamp: Date.now()
|
|
}));
|
|
|
|
console.log(` ${c('cyan', '→')} Sent credit_earned message`);
|
|
}, 500);
|
|
}
|
|
|
|
if (msg.type === 'task_assigned') {
|
|
console.log(` ${c('green', '✓')} Received task assignment: ${msg.task.id}`);
|
|
}
|
|
|
|
// Look for any acknowledgment
|
|
if (registered && (msg.type === 'time_crystal_sync' || msg.type === 'network_update')) {
|
|
console.log(` ${c('green', '✓')} Network processing credit update`);
|
|
|
|
clearTimeout(timeout);
|
|
ws.close();
|
|
this.pass('Credit Earning Flow');
|
|
resolve();
|
|
}
|
|
});
|
|
|
|
ws.on('error', (err) => {
|
|
clearTimeout(timeout);
|
|
this.fail('Credit Earning Flow', err.message);
|
|
resolve();
|
|
});
|
|
});
|
|
}
|
|
|
|
async testDashboardAccess() {
|
|
console.log(`\n${c('bold', '7. Testing Dashboard Access...')}`);
|
|
|
|
try {
|
|
const https = require('https');
|
|
|
|
await new Promise((resolve, reject) => {
|
|
https.get(CONFIG.dashboardUrl, (res) => {
|
|
if (res.statusCode === 200) {
|
|
console.log(` ${c('green', '✓')} Dashboard accessible (HTTP ${res.statusCode})`);
|
|
|
|
let data = '';
|
|
res.on('data', chunk => data += chunk);
|
|
res.on('end', () => {
|
|
if (data.includes('Edge-Net Dashboard')) {
|
|
console.log(` ${c('green', '✓')} Dashboard title found`);
|
|
this.pass('Dashboard Access', { url: CONFIG.dashboardUrl });
|
|
} else {
|
|
this.warn('Dashboard accessible but content unexpected');
|
|
this.pass('Dashboard Access (with warnings)');
|
|
}
|
|
resolve();
|
|
});
|
|
} else {
|
|
this.fail('Dashboard Access', `HTTP ${res.statusCode}`);
|
|
resolve();
|
|
}
|
|
}).on('error', (err) => {
|
|
this.fail('Dashboard Access', err.message);
|
|
resolve();
|
|
});
|
|
});
|
|
} catch (err) {
|
|
this.fail('Dashboard Access', err.message);
|
|
}
|
|
}
|
|
|
|
async testMultiDeviceSync() {
|
|
console.log(`\n${c('bold', '8. Testing Multi-Device Sync Capability...')}`);
|
|
|
|
try {
|
|
const meta = JSON.parse(await fs.readFile(CONFIG.identityPath, 'utf-8'));
|
|
const qdag = JSON.parse(await fs.readFile(CONFIG.qdagPath, 'utf-8'));
|
|
|
|
const myCredits = qdag.nodes
|
|
.filter(n => n.type === 'contribution' && n.contributor === meta.shortId)
|
|
.reduce((sum, c) => sum + (c.credits || 0), 0);
|
|
|
|
console.log(` ${c('green', '✓')} Identity exportable: ${meta.shortId}`);
|
|
console.log(` ${c('green', '✓')} QDAG contains contributor records: ${myCredits} credits`);
|
|
console.log(` ${c('green', '✓')} Sync protocol: Export identity → Import on Device 2 → Credits persist`);
|
|
console.log(` ${c('dim', 'Export command:')} node join.js --export backup.enc --password <secret>`);
|
|
console.log(` ${c('dim', 'Import command:')} node join.js --import backup.enc --password <secret>`);
|
|
|
|
this.pass('Multi-Device Sync Capability', {
|
|
exportable: true,
|
|
credits: myCredits,
|
|
});
|
|
} catch (err) {
|
|
this.fail('Multi-Device Sync Capability', err.message);
|
|
}
|
|
}
|
|
|
|
pass(test, details = {}) {
|
|
this.results.passed.push({ test, details });
|
|
}
|
|
|
|
fail(test, reason = '') {
|
|
this.results.failed.push({ test, reason });
|
|
}
|
|
|
|
warn(message) {
|
|
this.results.warnings.push(message);
|
|
}
|
|
|
|
printResults() {
|
|
console.log(`\n${c('bold', '═══════════════════════════════════════════════════')}`);
|
|
console.log(c('bold', ' VALIDATION RESULTS'));
|
|
console.log(`${c('bold', '═══════════════════════════════════════════════════')}\n`);
|
|
|
|
const total = this.results.passed.length + this.results.failed.length;
|
|
const passRate = total > 0 ? (this.results.passed.length / total * 100).toFixed(1) : 0;
|
|
|
|
console.log(`${c('green', '✓ PASSED:')} ${this.results.passed.length}`);
|
|
console.log(`${c('red', '✗ FAILED:')} ${this.results.failed.length}`);
|
|
console.log(`${c('yellow', '⚠ WARNINGS:')} ${this.results.warnings.length}`);
|
|
console.log(`${c('cyan', 'PASS RATE:')} ${passRate}%\n`);
|
|
|
|
if (this.results.failed.length > 0) {
|
|
console.log(c('red', 'FAILED TESTS:'));
|
|
this.results.failed.forEach(f => {
|
|
console.log(` ${c('red', '✗')} ${f.test}${f.reason ? ': ' + f.reason : ''}`);
|
|
});
|
|
console.log('');
|
|
}
|
|
|
|
if (this.results.warnings.length > 0) {
|
|
console.log(c('yellow', 'WARNINGS:'));
|
|
this.results.warnings.forEach(w => {
|
|
console.log(` ${c('yellow', '⚠')} ${w}`);
|
|
});
|
|
console.log('');
|
|
}
|
|
|
|
// Final verdict
|
|
console.log(`${c('bold', '═══════════════════════════════════════════════════')}`);
|
|
|
|
if (this.results.failed.length === 0) {
|
|
console.log(c('green', ' ✓ CONTRIBUTOR FLOW: 100% FUNCTIONAL'));
|
|
console.log(c('dim', ' All systems operational with secure QDAG persistence'));
|
|
} else {
|
|
console.log(c('red', ' ✗ CONTRIBUTOR FLOW: ISSUES DETECTED'));
|
|
console.log(c('dim', ` ${this.results.failed.length} test(s) failed - review above for details`));
|
|
}
|
|
|
|
console.log(`${c('bold', '═══════════════════════════════════════════════════')}\n`);
|
|
|
|
process.exit(this.results.failed.length > 0 ? 1 : 0);
|
|
}
|
|
}
|
|
|
|
// Run validation
|
|
const validator = new ContributorFlowValidator();
|
|
validator.run().catch(err => {
|
|
console.error(`\n${c('red', 'Fatal error:')} ${err.message}\n`);
|
|
process.exit(1);
|
|
});
|