mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-03-29 16:40:01 +00:00
Layer 1 (GPS, bridge-proof): Nodes with lat/lon are checked via haversine distance to the observer IATA center. Only nodes within 300km are considered regional. Bridged WA nodes appearing in SJC MQTT feeds are correctly rejected because their GPS coords are 1100km+ from SJC. Layer 2 (observer-based, fallback): Nodes without GPS fall back to _advertByObserver index — were they seen by a regional observer? Less precise but still useful for nodes that never sent ADVERTs with coordinates. Layer 3: Global fallback, flagged. New module: iata-coords.js with 60+ IATA airport coordinates + haversine distance function. API response now includes filterMethod (geo/observer/none) and distKm per conflict candidate. Tests: 22 unit tests (haversine, boundaries, cross-regional collision sim, layered fallback, bridge rejection).
97 lines
3.8 KiB
JavaScript
97 lines
3.8 KiB
JavaScript
#!/usr/bin/env node
|
|
// Integration test: Verify layered filtering works against live prod API
|
|
// Tests that resolve-hops returns regional metadata and correct filtering
|
|
|
|
const https = require('https');
|
|
const BASE = 'https://analyzer.00id.net';
|
|
|
|
function apiGet(path) {
|
|
return new Promise((resolve, reject) => {
|
|
https.get(BASE + path, { timeout: 10000 }, (res) => {
|
|
let data = '';
|
|
res.on('data', d => data += d);
|
|
res.on('end', () => { try { resolve(JSON.parse(data)); } catch (e) { reject(e); } });
|
|
}).on('error', reject);
|
|
});
|
|
}
|
|
|
|
let pass = 0, fail = 0;
|
|
function assert(condition, msg) {
|
|
if (condition) { pass++; console.log(` ✅ ${msg}`); }
|
|
else { fail++; console.error(` ❌ FAIL: ${msg}`); }
|
|
}
|
|
|
|
async function run() {
|
|
console.log('\n=== Integration: resolve-hops API with regional filtering ===\n');
|
|
|
|
// 1. Get a packet with short hops and a known observer
|
|
const packets = await apiGet('/api/packets?limit=100&groupByHash=true');
|
|
const pkt = packets.packets.find(p => {
|
|
const path = JSON.parse(p.path_json || '[]');
|
|
return path.length > 0 && path.some(h => h.length <= 2) && p.observer_id;
|
|
});
|
|
|
|
if (!pkt) {
|
|
console.log(' ⚠ No packets with short hops found — skipping API tests');
|
|
return;
|
|
}
|
|
|
|
const path = JSON.parse(pkt.path_json);
|
|
const shortHops = path.filter(h => h.length <= 2);
|
|
console.log(` Using packet ${pkt.hash.slice(0,12)} observed by ${pkt.observer_name || pkt.observer_id.slice(0,12)}`);
|
|
console.log(` Path: ${path.join(' → ')} (${shortHops.length} short hops)`);
|
|
|
|
// 2. Resolve WITH observer (should get regional filtering)
|
|
const withObs = await apiGet(`/api/resolve-hops?hops=${path.join(',')}&observer=${pkt.observer_id}`);
|
|
|
|
assert(withObs.region != null, `Response includes region: ${withObs.region}`);
|
|
|
|
// 3. Check that conflicts have filterMethod field
|
|
let hasFilterMethod = false;
|
|
let hasDistKm = false;
|
|
for (const [hop, info] of Object.entries(withObs.resolved)) {
|
|
if (info.conflicts && info.conflicts.length > 0) {
|
|
for (const c of info.conflicts) {
|
|
if (c.filterMethod) hasFilterMethod = true;
|
|
if (c.distKm != null) hasDistKm = true;
|
|
}
|
|
}
|
|
if (info.filterMethods) {
|
|
assert(Array.isArray(info.filterMethods), `Hop ${hop}: filterMethods is array: ${JSON.stringify(info.filterMethods)}`);
|
|
}
|
|
}
|
|
assert(hasFilterMethod, 'At least one conflict has filterMethod');
|
|
|
|
// 4. Resolve WITHOUT observer (no regional filtering)
|
|
const withoutObs = await apiGet(`/api/resolve-hops?hops=${path.join(',')}`);
|
|
assert(withoutObs.region === null, `Without observer: region is null`);
|
|
|
|
// 5. Compare: with observer should have same or fewer candidates per ambiguous hop
|
|
for (const hop of shortHops) {
|
|
const withInfo = withObs.resolved[hop];
|
|
const withoutInfo = withoutObs.resolved[hop];
|
|
if (withInfo && withoutInfo && withInfo.conflicts && withoutInfo.conflicts) {
|
|
const withCount = withInfo.totalRegional || withInfo.conflicts.length;
|
|
const withoutCount = withoutInfo.totalGlobal || withoutInfo.conflicts.length;
|
|
assert(withCount <= withoutCount + 1,
|
|
`Hop ${hop}: regional(${withCount}) <= global(${withoutCount}) — ${withInfo.name || '?'}`);
|
|
}
|
|
}
|
|
|
|
// 6. Check that geo-filtered candidates have distKm
|
|
for (const [hop, info] of Object.entries(withObs.resolved)) {
|
|
if (info.conflicts) {
|
|
const geoFiltered = info.conflicts.filter(c => c.filterMethod === 'geo');
|
|
for (const c of geoFiltered) {
|
|
assert(c.distKm != null, `Hop ${hop} candidate ${c.name}: has distKm=${c.distKm}km (geo filter)`);
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log(`\n${'='.repeat(40)}`);
|
|
console.log(`Results: ${pass} passed, ${fail} failed`);
|
|
process.exit(fail > 0 ? 1 : 0);
|
|
}
|
|
|
|
run().catch(e => { console.error('Test error:', e); process.exit(1); });
|