Files
meshcore-analyzer/test-compare-overlap.js
T
Kpa-clawbot d192330bdc feat(compare): asymmetric overlap stats for reference observer comparison (#671) (#1076)
## Summary

Adds asymmetric overlap percentages to the existing observer compare
page so it can be used as a **reference observer comparison** tool
(Uncle Lit's request, #671).

## What changed

`public/compare.js` (frontend only — no backend changes)

- New `computeOverlapStats(cmp)` helper that turns a
`comparePacketSets()` result into two-way coverage:
  - `aSeesOfB` — % of B's packets that A also saw
  - `bSeesOfA` — % of A's packets that B also saw
  - plus shared / onlyA / onlyB / totalA / totalB
- Two callout cards on the compare summary view:
  - `<A> saw N of <B>'s X packets` (Y%)
  - `<B> saw N of <A>'s X packets` (Y%)
- Existing "Only A / Only B / Both" tabs already identify unique
packets; that's the second half of the issue and is left intact.

## Operator workflow

Pick a known-good observer (LOS to key nodes) as the reference. Pair it
with a candidate. If the candidate's overlap with the reference is high
→ healthy. If low → investigate antenna, obstruction, or RF deafness.

## Out of scope (future work)

Issue lists several follow-on milestones — full Analytics sub-tab with
reference-vs-many table, SNR delta, geographic proximity filter,
server-side `/api/analytics/observer-comparison` endpoint. Those are
larger and tracked by the issue's M1-M4 milestones; this PR closes the
core ask (asymmetric overlap on the existing compare page) and leaves
the rest for follow-ups.

## Tests

`test-compare-overlap.js` — 6 unit tests via vm sandbox:

- exposes `computeOverlapStats` on `window`
- basic asymmetric scenario (8/10 vs 8/12)
- zero packets — no division by zero
- one observer empty — both percentages 0
- perfect overlap — 100% both ways
- disjoint observers — 0% both ways

TDD: red commit landed first with stub returning zeros (assertions
failed), green commit added the math.

Closes #671

---------

Co-authored-by: bot <bot@corescope.local>
2026-05-05 01:33:04 -07:00

112 lines
4.2 KiB
JavaScript

/* Unit tests for compare.js asymmetric overlap stats — Fixes #671 */
'use strict';
const vm = require('vm');
const fs = require('fs');
const assert = require('assert');
let passed = 0, failed = 0;
function test(name, fn) {
try { fn(); passed++; console.log(`${name}`); }
catch (e) { failed++; console.log(`${name}: ${e.message}`); }
}
function makeSandbox() {
const ctx = {
window: { addEventListener: () => {}, dispatchEvent: () => {} },
document: {
readyState: 'complete',
createElement: () => ({ id: '', textContent: '', innerHTML: '', addEventListener: () => {} }),
head: { appendChild: () => {} },
getElementById: () => null,
querySelectorAll: () => [],
addEventListener: () => {},
},
console,
setTimeout, clearTimeout,
location: { hash: '#/compare', href: '' },
history: { replaceState: () => {} },
URLSearchParams,
Map, Set, Date, Promise,
escapeHtml: (s) => s,
api: () => Promise.resolve({ observers: [] }),
CLIENT_TTL: { observers: 0 },
registerPage: () => {},
timeAgo: () => '',
payloadTypeColor: () => '',
};
ctx.self = ctx.window;
return ctx;
}
const ctx = makeSandbox();
const sandbox = vm.createContext(ctx);
const compareSrc = fs.readFileSync(__dirname + '/public/compare.js', 'utf8');
vm.runInContext(compareSrc, sandbox);
console.log('\ncompare.js asymmetric overlap stats (#671):');
test('computeOverlapStats is exposed on window', () => {
assert.strictEqual(typeof sandbox.window.computeOverlapStats, 'function',
'computeOverlapStats should be exposed on window');
});
test('basic asymmetric overlap — A sees 8/10 of B\'s, B sees 8/12 of A\'s', () => {
// A: 12 unique packets total (10 shared with B + 2 unique)
// B: 10 unique packets total (10 shared with A... wait: 8 shared + 2 unique to B)
// Let's do: A has packets 1..10 + extras 11,12; B has packets 1..8 + extras 13,14
// shared = {1..8} = 8
// onlyA = {9,10,11,12} = 4
// onlyB = {13,14} = 2
// A total = 12, B total = 10
// A sees 8/10 = 80% of B's packets
// B sees 8/12 = 66.7% of A's packets
const setA = new Set(['1','2','3','4','5','6','7','8','9','10','11','12']);
const setB = new Set(['1','2','3','4','5','6','7','8','13','14']);
const cmp = sandbox.window.comparePacketSets(setA, setB);
const stats = sandbox.window.computeOverlapStats(cmp);
assert.strictEqual(stats.totalA, 12, 'totalA');
assert.strictEqual(stats.totalB, 10, 'totalB');
assert.strictEqual(stats.shared, 8, 'shared');
assert.strictEqual(stats.onlyA, 4, 'onlyA');
assert.strictEqual(stats.onlyB, 2, 'onlyB');
assert.strictEqual(stats.aSeesOfB, 80.0, 'A sees 80% of B\'s');
assert.strictEqual(stats.bSeesOfA, Math.round(8/12*1000)/10, 'B sees 66.7% of A\'s');
});
test('zero packets — no division by zero', () => {
const cmp = sandbox.window.comparePacketSets(new Set(), new Set());
const stats = sandbox.window.computeOverlapStats(cmp);
assert.strictEqual(stats.aSeesOfB, 0);
assert.strictEqual(stats.bSeesOfA, 0);
assert.strictEqual(stats.shared, 0);
});
test('one observer empty — other gets 0% mutual coverage', () => {
const cmp = sandbox.window.comparePacketSets(new Set(['x','y']), new Set());
const stats = sandbox.window.computeOverlapStats(cmp);
assert.strictEqual(stats.totalA, 2);
assert.strictEqual(stats.totalB, 0);
assert.strictEqual(stats.aSeesOfB, 0, 'no B packets to see');
assert.strictEqual(stats.bSeesOfA, 0, 'B saw 0 of A\'s');
});
test('perfect overlap — 100% both ways', () => {
const s = new Set(['a','b','c']);
const cmp = sandbox.window.comparePacketSets(s, new Set(s));
const stats = sandbox.window.computeOverlapStats(cmp);
assert.strictEqual(stats.aSeesOfB, 100);
assert.strictEqual(stats.bSeesOfA, 100);
assert.strictEqual(stats.shared, 3);
});
test('disjoint observers — 0% both ways', () => {
const cmp = sandbox.window.comparePacketSets(new Set(['a','b']), new Set(['c','d']));
const stats = sandbox.window.computeOverlapStats(cmp);
assert.strictEqual(stats.aSeesOfB, 0);
assert.strictEqual(stats.bSeesOfA, 0);
assert.strictEqual(stats.shared, 0);
});
console.log(`\n ${passed} passed, ${failed} failed`);
process.exit(failed > 0 ? 1 : 0);