mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-03-30 15:55:49 +00:00
- Create inactive_nodes table with identical schema to nodes - Add retention.nodeDays config (default 7) in Node.js and Go - On startup: move nodes not seen in N days to inactive_nodes - Daily timer (24h setInterval / goroutine ticker) repeats the move - Log 'Moved X nodes to inactive_nodes (not seen in N days)' - All existing queries unchanged — they only read nodes table - Add 14 new tests for moveStaleNodes in test-db.js - Both Node (db.js/server.js) and Go (ingestor/server) implemented Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
513 lines
20 KiB
JavaScript
513 lines
20 KiB
JavaScript
'use strict';
|
|
|
|
// Test db.js functions with a temp database
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
|
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'meshcore-db-test-'));
|
|
const dbPath = path.join(tmpDir, 'test.db');
|
|
process.env.DB_PATH = dbPath;
|
|
|
|
// Now require db.js — it will use our temp DB
|
|
const db = require('./db');
|
|
|
|
let passed = 0, failed = 0;
|
|
function assert(cond, msg) {
|
|
if (cond) { passed++; console.log(` ✅ ${msg}`); }
|
|
else { failed++; console.error(` ❌ ${msg}`); }
|
|
}
|
|
|
|
function cleanup() {
|
|
try { db.db.close(); } catch {}
|
|
try { fs.rmSync(tmpDir, { recursive: true }); } catch {}
|
|
}
|
|
|
|
console.log('── db.js tests ──\n');
|
|
|
|
// --- Schema ---
|
|
console.log('Schema:');
|
|
{
|
|
const tables = db.db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map(r => r.name);
|
|
assert(tables.includes('nodes'), 'nodes table exists');
|
|
assert(tables.includes('observers'), 'observers table exists');
|
|
assert(tables.includes('transmissions'), 'transmissions table exists');
|
|
assert(tables.includes('observations'), 'observations table exists');
|
|
}
|
|
|
|
// --- upsertNode ---
|
|
console.log('\nupsertNode:');
|
|
{
|
|
db.upsertNode({ public_key: 'aabbccdd11223344aabbccdd11223344', name: 'TestNode', role: 'repeater', lat: 37.0, lon: -122.0 });
|
|
const node = db.getNode('aabbccdd11223344aabbccdd11223344');
|
|
assert(node !== null, 'node inserted');
|
|
assert(node.name === 'TestNode', 'name correct');
|
|
assert(node.role === 'repeater', 'role correct');
|
|
assert(node.lat === 37.0, 'lat correct');
|
|
|
|
// Update
|
|
db.upsertNode({ public_key: 'aabbccdd11223344aabbccdd11223344', name: 'UpdatedNode', role: 'room' });
|
|
const node2 = db.getNode('aabbccdd11223344aabbccdd11223344');
|
|
assert(node2.name === 'UpdatedNode', 'name updated');
|
|
assert(node2.name === 'UpdatedNode', 'name updated');
|
|
assert(node2.advert_count === 0, 'advert_count unchanged by upsertNode');
|
|
|
|
// advert_count only increments via incrementAdvertCount
|
|
db.incrementAdvertCount('aabbccdd11223344aabbccdd11223344');
|
|
const node3 = db.getNode('aabbccdd11223344aabbccdd11223344');
|
|
assert(node3.advert_count === 1, 'advert_count incremented via incrementAdvertCount');
|
|
}
|
|
|
|
// --- upsertObserver ---
|
|
console.log('\nupsertObserver:');
|
|
{
|
|
db.upsertObserver({ id: 'obs-1', name: 'Observer One', iata: 'SFO' });
|
|
const observers = db.getObservers();
|
|
assert(observers.length >= 1, 'observer inserted');
|
|
assert(observers.some(o => o.id === 'obs-1'), 'observer found by id');
|
|
assert(observers.find(o => o.id === 'obs-1').name === 'Observer One', 'observer name correct');
|
|
|
|
// Upsert again
|
|
db.upsertObserver({ id: 'obs-1', name: 'Observer Updated' });
|
|
const obs2 = db.getObservers().find(o => o.id === 'obs-1');
|
|
assert(obs2.name === 'Observer Updated', 'observer name updated');
|
|
assert(obs2.packet_count === 2, 'packet_count incremented');
|
|
}
|
|
|
|
// --- updateObserverStatus ---
|
|
console.log('\nupdateObserverStatus:');
|
|
{
|
|
db.updateObserverStatus({ id: 'obs-2', name: 'Status Observer', iata: 'LAX', model: 'T-Deck' });
|
|
const obs = db.getObservers().find(o => o.id === 'obs-2');
|
|
assert(obs !== null, 'observer created via status update');
|
|
assert(obs.model === 'T-Deck', 'model set');
|
|
assert(obs.packet_count === 0, 'packet_count stays 0 for status update');
|
|
}
|
|
|
|
// --- insertTransmission ---
|
|
console.log('\ninsertTransmission:');
|
|
{
|
|
const result = db.insertTransmission({
|
|
raw_hex: '0400aabbccdd',
|
|
hash: 'hash-001',
|
|
timestamp: '2025-01-01T00:00:00Z',
|
|
observer_id: 'obs-1',
|
|
observer_name: 'Observer One',
|
|
direction: 'rx',
|
|
snr: 10.5,
|
|
rssi: -85,
|
|
route_type: 1,
|
|
payload_type: 4,
|
|
payload_version: 1,
|
|
path_json: '["aabb","ccdd"]',
|
|
decoded_json: '{"type":"ADVERT","pubKey":"aabbccdd11223344aabbccdd11223344","name":"TestNode"}',
|
|
});
|
|
assert(result !== null, 'transmission inserted');
|
|
assert(result.transmissionId > 0, 'has transmissionId');
|
|
assert(result.observationId > 0, 'has observationId');
|
|
|
|
// Duplicate hash = same transmission, new observation
|
|
const result2 = db.insertTransmission({
|
|
raw_hex: '0400aabbccdd',
|
|
hash: 'hash-001',
|
|
timestamp: '2025-01-01T00:01:00Z',
|
|
observer_id: 'obs-2',
|
|
observer_name: 'Observer Two',
|
|
direction: 'rx',
|
|
snr: 8.0,
|
|
rssi: -90,
|
|
route_type: 1,
|
|
payload_type: 4,
|
|
path_json: '["aabb"]',
|
|
decoded_json: '{"type":"ADVERT","pubKey":"aabbccdd11223344aabbccdd11223344","name":"TestNode"}',
|
|
});
|
|
assert(result2.transmissionId === result.transmissionId, 'same transmissionId for duplicate hash');
|
|
|
|
// No hash = null
|
|
const result3 = db.insertTransmission({ raw_hex: '0400' });
|
|
assert(result3 === null, 'no hash returns null');
|
|
}
|
|
|
|
// --- getPackets ---
|
|
console.log('\ngetPackets:');
|
|
{
|
|
const { rows, total } = db.getPackets({ limit: 10 });
|
|
assert(total >= 1, 'has packets');
|
|
assert(rows.length >= 1, 'returns rows');
|
|
assert(rows[0].hash === 'hash-001', 'correct hash');
|
|
|
|
// Filter by type
|
|
const { rows: r2 } = db.getPackets({ type: 4 });
|
|
assert(r2.length >= 1, 'filter by type works');
|
|
|
|
const { rows: r3 } = db.getPackets({ type: 99 });
|
|
assert(r3.length === 0, 'filter by nonexistent type returns empty');
|
|
|
|
// Filter by hash
|
|
const { rows: r4 } = db.getPackets({ hash: 'hash-001' });
|
|
assert(r4.length >= 1, 'filter by hash works');
|
|
}
|
|
|
|
// --- getPacket ---
|
|
console.log('\ngetPacket:');
|
|
{
|
|
const { rows } = db.getPackets({ limit: 1 });
|
|
const pkt = db.getPacket(rows[0].id);
|
|
assert(pkt !== null, 'getPacket returns packet');
|
|
assert(pkt.hash === 'hash-001', 'correct packet');
|
|
|
|
const missing = db.getPacket(999999);
|
|
assert(missing === null, 'missing packet returns null');
|
|
}
|
|
|
|
// --- getTransmission ---
|
|
console.log('\ngetTransmission:');
|
|
{
|
|
const tx = db.getTransmission(1);
|
|
assert(tx !== null, 'getTransmission returns data');
|
|
assert(tx.hash === 'hash-001', 'correct hash');
|
|
|
|
const missing = db.getTransmission(999999);
|
|
assert(missing === null, 'missing transmission returns null');
|
|
}
|
|
|
|
// --- getNodes ---
|
|
console.log('\ngetNodes:');
|
|
{
|
|
const { rows, total } = db.getNodes({ limit: 10 });
|
|
assert(total >= 1, 'has nodes');
|
|
assert(rows.length >= 1, 'returns node rows');
|
|
|
|
// Sort by name
|
|
const { rows: r2 } = db.getNodes({ sortBy: 'name' });
|
|
assert(r2.length >= 1, 'sort by name works');
|
|
|
|
// Invalid sort falls back to last_seen
|
|
const { rows: r3 } = db.getNodes({ sortBy: 'DROP TABLE nodes' });
|
|
assert(r3.length >= 1, 'invalid sort is safe');
|
|
}
|
|
|
|
// --- getNode ---
|
|
console.log('\ngetNode:');
|
|
{
|
|
const node = db.getNode('aabbccdd11223344aabbccdd11223344');
|
|
assert(node !== null, 'getNode returns node');
|
|
assert(Array.isArray(node.recentPackets), 'has recentPackets');
|
|
|
|
const missing = db.getNode('nonexistent');
|
|
assert(missing === null, 'missing node returns null');
|
|
}
|
|
|
|
// --- searchNodes ---
|
|
console.log('\nsearchNodes:');
|
|
{
|
|
const results = db.searchNodes('Updated');
|
|
assert(results.length >= 1, 'search by name');
|
|
|
|
const r2 = db.searchNodes('aabbcc');
|
|
assert(r2.length >= 1, 'search by pubkey prefix');
|
|
|
|
const r3 = db.searchNodes('nonexistent_xyz');
|
|
assert(r3.length === 0, 'no results for nonexistent');
|
|
}
|
|
|
|
// --- getStats ---
|
|
console.log('\ngetStats:');
|
|
{
|
|
const stats = db.getStats();
|
|
assert(stats.totalNodes >= 1, 'totalNodes');
|
|
assert(stats.totalObservers >= 1, 'totalObservers');
|
|
assert(typeof stats.totalPackets === 'number', 'totalPackets is number');
|
|
assert(typeof stats.packetsLastHour === 'number', 'packetsLastHour is number');
|
|
assert(typeof stats.totalNodesAllTime === 'number', 'totalNodesAllTime is number');
|
|
assert(stats.totalNodesAllTime >= stats.totalNodes, 'totalNodesAllTime >= totalNodes');
|
|
}
|
|
|
|
// --- getStats active node filtering ---
|
|
console.log('\ngetStats active node filtering:');
|
|
{
|
|
// Insert a node with last_seen 30 days ago (should be excluded from totalNodes)
|
|
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 3600000).toISOString();
|
|
db.upsertNode({ public_key: 'deadnode0000000000000000deadnode00', name: 'DeadNode', role: 'repeater', last_seen: thirtyDaysAgo, first_seen: thirtyDaysAgo });
|
|
|
|
// Insert a node with last_seen now (should be included)
|
|
db.upsertNode({ public_key: 'livenode0000000000000000livenode00', name: 'LiveNode', role: 'companion', last_seen: new Date().toISOString() });
|
|
|
|
const stats = db.getStats();
|
|
assert(stats.totalNodesAllTime > stats.totalNodes, 'dead node excluded from totalNodes but included in totalNodesAllTime');
|
|
|
|
// Verify the dead node is in totalNodesAllTime
|
|
const allTime = stats.totalNodesAllTime;
|
|
assert(allTime >= 3, 'totalNodesAllTime includes dead + live nodes');
|
|
|
|
// Verify active count doesn't include the 30-day-old node
|
|
// The dead node's last_seen is 30 days ago, window is 7 days
|
|
const nodeInDb = db.getNode('deadnode0000000000000000deadnode00');
|
|
assert(nodeInDb !== null, 'dead node exists in DB');
|
|
const liveNode = db.getNode('livenode0000000000000000livenode00');
|
|
assert(liveNode !== null, 'live node exists in DB');
|
|
}
|
|
|
|
// --- getNodeHealth ---
|
|
console.log('\ngetNodeHealth:');
|
|
{
|
|
const health = db.getNodeHealth('aabbccdd11223344aabbccdd11223344');
|
|
assert(health !== null, 'returns health data');
|
|
assert(health.node.name === 'UpdatedNode', 'has node info');
|
|
assert(typeof health.stats.totalPackets === 'number', 'has totalPackets stat');
|
|
assert(Array.isArray(health.observers), 'has observers array');
|
|
assert(Array.isArray(health.recentPackets), 'has recentPackets array');
|
|
|
|
const missing = db.getNodeHealth('nonexistent');
|
|
assert(missing === null, 'missing node returns null');
|
|
}
|
|
|
|
// --- getNodeAnalytics ---
|
|
console.log('\ngetNodeAnalytics:');
|
|
{
|
|
const analytics = db.getNodeAnalytics('aabbccdd11223344aabbccdd11223344', 7);
|
|
assert(analytics !== null, 'returns analytics');
|
|
assert(analytics.node.name === 'UpdatedNode', 'has node info');
|
|
assert(Array.isArray(analytics.activityTimeline), 'has activityTimeline');
|
|
assert(Array.isArray(analytics.snrTrend), 'has snrTrend');
|
|
assert(Array.isArray(analytics.packetTypeBreakdown), 'has packetTypeBreakdown');
|
|
assert(Array.isArray(analytics.observerCoverage), 'has observerCoverage');
|
|
assert(Array.isArray(analytics.hopDistribution), 'has hopDistribution');
|
|
assert(Array.isArray(analytics.peerInteractions), 'has peerInteractions');
|
|
assert(Array.isArray(analytics.uptimeHeatmap), 'has uptimeHeatmap');
|
|
assert(typeof analytics.computedStats.availabilityPct === 'number', 'has availabilityPct');
|
|
assert(typeof analytics.computedStats.signalGrade === 'string', 'has signalGrade');
|
|
|
|
const missing = db.getNodeAnalytics('nonexistent', 7);
|
|
assert(missing === null, 'missing node returns null');
|
|
}
|
|
|
|
// --- seed ---
|
|
console.log('\nseed:');
|
|
{
|
|
if (typeof db.seed === 'function') {
|
|
// Already has data, should return false
|
|
const result = db.seed();
|
|
assert(result === false, 'seed returns false when data exists');
|
|
} else {
|
|
console.log(' (skipped — seed not exported)');
|
|
}
|
|
}
|
|
|
|
// --- v3 schema tests (fresh DB should be v3) ---
|
|
console.log('\nv3 schema:');
|
|
{
|
|
assert(db.schemaVersion >= 3, 'fresh DB creates v3 schema');
|
|
|
|
// observations table should have observer_idx, not observer_id
|
|
const cols = db.db.pragma('table_info(observations)').map(c => c.name);
|
|
assert(cols.includes('observer_idx'), 'observations has observer_idx column');
|
|
assert(!cols.includes('observer_id'), 'observations does NOT have observer_id column');
|
|
assert(!cols.includes('observer_name'), 'observations does NOT have observer_name column');
|
|
assert(!cols.includes('hash'), 'observations does NOT have hash column');
|
|
assert(!cols.includes('created_at'), 'observations does NOT have created_at column');
|
|
|
|
// timestamp should be integer
|
|
const obsRow = db.db.prepare('SELECT typeof(timestamp) as t FROM observations LIMIT 1').get();
|
|
if (obsRow) {
|
|
assert(obsRow.t === 'integer', 'timestamp is stored as integer');
|
|
}
|
|
|
|
// packets_v view should still expose observer_id, observer_name, ISO timestamp
|
|
const viewRow = db.db.prepare('SELECT * FROM packets_v LIMIT 1').get();
|
|
if (viewRow) {
|
|
assert('observer_id' in viewRow, 'packets_v exposes observer_id');
|
|
assert('observer_name' in viewRow, 'packets_v exposes observer_name');
|
|
assert(typeof viewRow.timestamp === 'string', 'packets_v timestamp is ISO string');
|
|
}
|
|
|
|
// user_version is 3
|
|
const sv = db.db.pragma('user_version', { simple: true });
|
|
assert(sv === 3, 'user_version is 3');
|
|
}
|
|
|
|
// --- v3 ingestion: observer resolved via observer_idx ---
|
|
console.log('\nv3 ingestion with observer resolution:');
|
|
{
|
|
// Insert a new observer
|
|
db.upsertObserver({ id: 'obs-v3-test', name: 'V3 Test Observer' });
|
|
|
|
// Insert observation referencing that observer
|
|
const result = db.insertTransmission({
|
|
raw_hex: '0400deadbeef',
|
|
hash: 'hash-v3-001',
|
|
timestamp: '2025-06-01T12:00:00Z',
|
|
observer_id: 'obs-v3-test',
|
|
observer_name: 'V3 Test Observer',
|
|
direction: 'rx',
|
|
snr: 12.0,
|
|
rssi: -80,
|
|
route_type: 1,
|
|
payload_type: 4,
|
|
path_json: '["aabb"]',
|
|
});
|
|
assert(result !== null, 'v3 insertion succeeded');
|
|
assert(result.transmissionId > 0, 'v3 has transmissionId');
|
|
|
|
// Verify via packets_v view
|
|
const pkt = db.db.prepare('SELECT * FROM packets_v WHERE hash = ?').get('hash-v3-001');
|
|
assert(pkt !== null, 'v3 packet found via view');
|
|
assert(pkt.observer_id === 'obs-v3-test', 'v3 observer_id resolved in view');
|
|
assert(pkt.observer_name === 'V3 Test Observer', 'v3 observer_name resolved in view');
|
|
assert(typeof pkt.timestamp === 'string', 'v3 timestamp is ISO string in view');
|
|
assert(pkt.timestamp.includes('2025-06-01'), 'v3 timestamp date correct');
|
|
|
|
// Raw observation should have integer timestamp
|
|
const obs = db.db.prepare('SELECT * FROM observations ORDER BY id DESC LIMIT 1').get();
|
|
assert(typeof obs.timestamp === 'number', 'v3 raw observation timestamp is integer');
|
|
assert(obs.observer_idx !== null, 'v3 observation has observer_idx');
|
|
}
|
|
|
|
// --- v3 dedup ---
|
|
console.log('\nv3 dedup:');
|
|
{
|
|
// Insert same observation again — should be deduped
|
|
const result = db.insertTransmission({
|
|
raw_hex: '0400deadbeef',
|
|
hash: 'hash-v3-001',
|
|
timestamp: '2025-06-01T12:00:00Z',
|
|
observer_id: 'obs-v3-test',
|
|
direction: 'rx',
|
|
snr: 12.0,
|
|
rssi: -80,
|
|
path_json: '["aabb"]',
|
|
});
|
|
assert(result.observationId === 0, 'duplicate caught by in-memory dedup');
|
|
|
|
// Different observer = not a dupe
|
|
db.upsertObserver({ id: 'obs-v3-test-2', name: 'V3 Test Observer 2' });
|
|
const result2 = db.insertTransmission({
|
|
raw_hex: '0400deadbeef',
|
|
hash: 'hash-v3-001',
|
|
timestamp: '2025-06-01T12:01:00Z',
|
|
observer_id: 'obs-v3-test-2',
|
|
direction: 'rx',
|
|
snr: 9.0,
|
|
rssi: -88,
|
|
path_json: '["ccdd"]',
|
|
});
|
|
assert(result2.observationId > 0, 'different observer is not a dupe');
|
|
}
|
|
|
|
// --- removePhantomNodes ---
|
|
console.log('\nremovePhantomNodes:');
|
|
{
|
|
// Insert phantom nodes (short public_keys like hop prefixes)
|
|
db.upsertNode({ public_key: 'aabb', name: null, role: 'repeater' });
|
|
db.upsertNode({ public_key: 'ccddee', name: null, role: 'repeater' });
|
|
db.upsertNode({ public_key: 'ff001122', name: null, role: 'repeater' });
|
|
db.upsertNode({ public_key: '0011223344556677', name: null, role: 'repeater' }); // 16 chars — still phantom
|
|
|
|
// Verify they exist
|
|
assert(db.getNode('aabb') !== null, 'phantom node aabb exists before cleanup');
|
|
assert(db.getNode('ccddee') !== null, 'phantom node ccddee exists before cleanup');
|
|
assert(db.getNode('ff001122') !== null, 'phantom node ff001122 exists before cleanup');
|
|
assert(db.getNode('0011223344556677') !== null, 'phantom 16-char exists before cleanup');
|
|
|
|
// Verify real node still exists
|
|
assert(db.getNode('aabbccdd11223344aabbccdd11223344') !== null, 'real node exists before cleanup');
|
|
|
|
// Run cleanup
|
|
const removed = db.removePhantomNodes();
|
|
assert(removed === 4, `removed 4 phantom nodes (got ${removed})`);
|
|
|
|
// Verify phantoms are gone
|
|
assert(db.getNode('aabb') === null, 'phantom aabb removed');
|
|
assert(db.getNode('ccddee') === null, 'phantom ccddee removed');
|
|
assert(db.getNode('ff001122') === null, 'phantom ff001122 removed');
|
|
assert(db.getNode('0011223344556677') === null, 'phantom 16-char removed');
|
|
|
|
// Verify real node is still there
|
|
assert(db.getNode('aabbccdd11223344aabbccdd11223344') !== null, 'real node preserved after cleanup');
|
|
|
|
// Running again should remove 0
|
|
const removed2 = db.removePhantomNodes();
|
|
assert(removed2 === 0, 'second cleanup removes nothing');
|
|
}
|
|
|
|
// --- stats exclude phantom nodes ---
|
|
console.log('\nstats exclude phantom nodes:');
|
|
{
|
|
const statsBefore = db.getStats();
|
|
const countBefore = statsBefore.totalNodesAllTime;
|
|
|
|
// Insert a phantom — should be cleanable
|
|
db.upsertNode({ public_key: 'deadbeef', name: null, role: 'repeater' });
|
|
const statsWithPhantom = db.getStats();
|
|
assert(statsWithPhantom.totalNodesAllTime === countBefore + 1, 'phantom inflates totalNodesAllTime');
|
|
|
|
// Clean it
|
|
db.removePhantomNodes();
|
|
const statsAfter = db.getStats();
|
|
assert(statsAfter.totalNodesAllTime === countBefore, 'phantom removed from totalNodesAllTime');
|
|
}
|
|
|
|
// --- moveStaleNodes ---
|
|
console.log('\nmoveStaleNodes:');
|
|
{
|
|
// Verify inactive_nodes table exists
|
|
const tables = db.db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map(r => r.name);
|
|
assert(tables.includes('inactive_nodes'), 'inactive_nodes table exists');
|
|
|
|
// Verify inactive_nodes has same columns as nodes
|
|
const nodesCols = db.db.pragma('table_info(nodes)').map(c => c.name).sort();
|
|
const inactiveCols = db.db.pragma('table_info(inactive_nodes)').map(c => c.name).sort();
|
|
assert(JSON.stringify(nodesCols) === JSON.stringify(inactiveCols), 'inactive_nodes has same columns as nodes');
|
|
|
|
// Insert a stale node (last_seen 30 days ago) and a fresh node
|
|
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 3600000).toISOString();
|
|
const now = new Date().toISOString();
|
|
db.upsertNode({ public_key: 'stale00000000000000000000stale000', name: 'StaleNode', role: 'repeater', last_seen: thirtyDaysAgo, first_seen: thirtyDaysAgo });
|
|
db.upsertNode({ public_key: 'fresh00000000000000000000fresh000', name: 'FreshNode', role: 'companion', last_seen: now, first_seen: now });
|
|
|
|
// Verify both exist in nodes
|
|
assert(db.getNode('stale00000000000000000000stale000') !== null, 'stale node exists before move');
|
|
assert(db.getNode('fresh00000000000000000000fresh000') !== null, 'fresh node exists before move');
|
|
|
|
// Move stale nodes (7 day threshold)
|
|
const moved = db.moveStaleNodes(7);
|
|
assert(moved >= 1, `moveStaleNodes moved at least 1 node (got ${moved})`);
|
|
|
|
// Stale node should be gone from nodes
|
|
assert(db.getNode('stale00000000000000000000stale000') === null, 'stale node removed from nodes');
|
|
|
|
// Fresh node should still be in nodes
|
|
assert(db.getNode('fresh00000000000000000000fresh000') !== null, 'fresh node still in nodes');
|
|
|
|
// Stale node should be in inactive_nodes
|
|
const inactive = db.db.prepare('SELECT * FROM inactive_nodes WHERE public_key = ?').get('stale00000000000000000000stale000');
|
|
assert(inactive !== null, 'stale node exists in inactive_nodes');
|
|
assert(inactive.name === 'StaleNode', 'stale node name preserved in inactive_nodes');
|
|
assert(inactive.role === 'repeater', 'stale node role preserved in inactive_nodes');
|
|
|
|
// Fresh node should NOT be in inactive_nodes
|
|
const freshInactive = db.db.prepare('SELECT * FROM inactive_nodes WHERE public_key = ?').get('fresh00000000000000000000fresh000');
|
|
assert(!freshInactive, 'fresh node not in inactive_nodes');
|
|
|
|
// Running again should move 0 (already moved)
|
|
const moved2 = db.moveStaleNodes(7);
|
|
assert(moved2 === 0, 'second moveStaleNodes moves nothing');
|
|
|
|
// With nodeDays=0 should be a no-op
|
|
const moved3 = db.moveStaleNodes(0);
|
|
assert(moved3 === 0, 'moveStaleNodes(0) is a no-op');
|
|
|
|
// With null should be a no-op
|
|
const moved4 = db.moveStaleNodes(null);
|
|
assert(moved4 === 0, 'moveStaleNodes(null) is a no-op');
|
|
}
|
|
|
|
cleanup();
|
|
delete process.env.DB_PATH;
|
|
|
|
console.log(`\n═══════════════════════════════════════`);
|
|
console.log(` PASSED: ${passed}`);
|
|
console.log(` FAILED: ${failed}`);
|
|
console.log(`═══════════════════════════════════════`);
|
|
if (failed > 0) process.exit(1);
|