mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-09-26 17:07:53 +00:00
## Summary Replaces the three drifted per-surface payload-type label vocabularies with a single canonical map keyed by firmware enum name. Per the locked triage comment on #1799 ([comment-4823975431](https://github.com/Kpa-clawbot/CoreScope/issues/1799#issuecomment-4823975431)): > Create `public/payload-labels.js` exporting `{GRP_DATA: {short:'Group Data', long:'Group data packet', enumId:6}, ...}`. Migrate `packets.js typeMap`, `packet-filter.js FW_PAYLOAD_TYPES`, `live.js TYPE_COLORS legend` to consume it. E2E that scrapes each surface and asserts label equality. ## Changes - **`public/payload-labels.js`** (new) — canonical map exposed as `window.PayloadLabels` and `window.PayloadLabelsApi`. Keys are firmware enum names; values carry `{short, long, enumId}` plus derived `SHORT_BY_ID` / `FW_PAYLOAD_TYPES` / `TYPE_ALIASES` for legacy callers. - **`public/packets.js`** — `TYPE_NAMES` + `typeMap` now read from `PayloadLabelsApi.SHORT_BY_ID`. Literal kept only as a defensive fallback for the case where the script tag fails to load. - **`public/packet-filter.js`** — `FW_PAYLOAD_TYPES` + `TYPE_ALIASES` now sourced from `PayloadLabelsApi`. Literal fallback retained so `node test-packet-filter.js` still works headlessly. - **`public/live.js`** — legend `<li>` rows are now generated from `window.PayloadLabels` in stable order, killing the third-vocabulary `Message — Group text` / `Direct — Direct message` drift the #1797 review surfaced. - **`public/index.html`** — `<script src="payload-labels.js">` loaded before `roles.js` / `packet-filter.js` / `packets.js`. - **`test-issue-1799-label-vocab-e2e.js`** (new) — Playwright E2E. Scrapes `#liveLegend` rows and the `/packets` type-filter checklist, asserts each label matches `window.PayloadLabels[ENUM].short` for `TXT_MSG`, `GRP_TXT`, `GRP_DATA`. Also verifies `window.PacketFilter` still recognises the enum names. - **`.github/workflows/deploy.yml`** — wired the new E2E into the existing Playwright block. ## TDD trail - Red commit `eb392d4` — adds the failing E2E only (asserts `window.PayloadLabels` exists and labels match; both fail). - Green commit `44e902a` — introduces the canonical map and migrates the three surfaces. ## Verification - `node test-packet-filter.js` — 92/92 pass with the new fallback wiring. - Preflight: `bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master` — clean. Browser verified: E2E `test-issue-1799-label-vocab-e2e.js` exercises `/live` legend + `/packets` type filter against a Playwright headless Chromium; CI's Playwright block runs it on every push. E2E assertion added: `test-issue-1799-label-vocab-e2e.js:139` — `assert(fromLegend === canon, ...)` and `assert(fromPackets === canon, ...)` per enum. Fixes #1799 --------- Co-authored-by: mc-bot <bot@corescope> Co-authored-by: openclaw-bot <bot@openclaw.local> Co-authored-by: clawbot <clawbot@kpa.com> Co-authored-by: clawbot <bot@clawbot.local>
449 lines
20 KiB
JavaScript
449 lines
20 KiB
JavaScript
/* test-issue-1633-hide-1byte-hops.js
|
|
* #1633 — customizer toggle that hides 1-byte path hops at every
|
|
* render site. Render-time only; firmware/store untouched.
|
|
*
|
|
* Asserts:
|
|
* 1. Defaults OFF (back-compat: no surprise for existing operators).
|
|
* 2. With hide1ByteHops=true a mixed path renders ONLY multi-byte hops.
|
|
* 3. With hide1ByteHops=false the full path renders.
|
|
* 4. HopDisplay.renderPath emits exactly the multi-byte chips when ON.
|
|
* 5. Map polyline source: positions tagged with 1-byte _hopHex are dropped
|
|
* from the polyline path when ON; 2/3-byte hops survive; origin /
|
|
* destination (no _hopHex) are always kept.
|
|
* 6. Analytics route-pattern aggregation: rebuilt path key contains ONLY
|
|
* multi-byte hop hexes when ON.
|
|
*/
|
|
'use strict';
|
|
|
|
const assert = require('assert');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const vm = require('vm');
|
|
|
|
let passed = 0, failed = 0;
|
|
function test(name, fn) {
|
|
try { fn(); passed++; console.log(' ✅ ' + name); }
|
|
catch (e) { failed++; console.log(' ❌ ' + name + ': ' + e.message); }
|
|
}
|
|
|
|
// ── DOM-less sandbox so hop-display.js / hop-filter.js load cleanly ──
|
|
function makeSandbox() {
|
|
const store = {};
|
|
const ctx = {
|
|
window: {
|
|
addEventListener: () => {},
|
|
dispatchEvent: () => {},
|
|
CustomEvent: function (n, d) { this.type = n; this.detail = (d && d.detail) || null; }
|
|
},
|
|
document: {
|
|
readyState: 'complete',
|
|
createElement: () => ({ id: '', textContent: '', innerHTML: '', dataset: {}, setAttribute(){}, getAttribute(){return null;} }),
|
|
head: { appendChild: () => {} },
|
|
body: { appendChild: () => {} },
|
|
getElementById: () => null,
|
|
addEventListener: () => {},
|
|
querySelectorAll: () => [],
|
|
querySelector: () => null,
|
|
},
|
|
localStorage: {
|
|
getItem: k => (k in store ? store[k] : null),
|
|
setItem: (k, v) => { store[k] = String(v); },
|
|
removeItem: k => { delete store[k]; },
|
|
clear: () => { for (const k of Object.keys(store)) delete store[k]; }
|
|
},
|
|
console, Math, Date, Array, Object, String, Number, JSON, Boolean,
|
|
module: { exports: {} }, exports: {},
|
|
};
|
|
ctx.window.localStorage = ctx.localStorage;
|
|
vm.createContext(ctx);
|
|
return ctx;
|
|
}
|
|
|
|
function load(ctx, file) {
|
|
const src = fs.readFileSync(path.join(__dirname, file), 'utf8');
|
|
vm.runInContext(src, ctx, { filename: file });
|
|
}
|
|
|
|
// === Build a sandbox with hop-filter + hop-display loaded ===
|
|
function freshSandbox() {
|
|
const ctx = makeSandbox();
|
|
load(ctx, 'public/hop-filter.js');
|
|
load(ctx, 'public/hop-display.js');
|
|
return ctx;
|
|
}
|
|
|
|
// === Heavier sandbox that loads live.js (mirrors test-live.js pattern) ===
|
|
function makeLiveSandbox() {
|
|
const ctx = {
|
|
window: {
|
|
addEventListener: () => {}, dispatchEvent: () => {}, devicePixelRatio: 1,
|
|
CustomEvent: function (n, d) { this.type = n; this.detail = (d && d.detail) || null; },
|
|
},
|
|
document: {
|
|
readyState: 'complete',
|
|
createElement: () => ({
|
|
tagName: '', id: '', textContent: '', innerHTML: '', style: {}, dataset: {},
|
|
classList: { add(){}, remove(){}, contains(){return false;} },
|
|
setAttribute(){}, getAttribute(){return null;},
|
|
addEventListener(){}, focus(){},
|
|
getContext: () => ({ clearRect(){}, fillRect(){}, beginPath(){}, arc(){}, fill(){}, scale(){}, fillStyle: '', font: '', fillText(){} }),
|
|
offsetWidth: 200, offsetHeight: 40, width: 0, height: 0,
|
|
}),
|
|
head: { appendChild: () => {} },
|
|
body: { appendChild: () => {}, removeChild: () => {}, contains: () => false },
|
|
getElementById: () => null,
|
|
addEventListener: () => {},
|
|
querySelectorAll: () => [],
|
|
querySelector: () => null,
|
|
createElementNS: () => ({ tagName: 'svg', setAttribute(){}, getAttribute(){return null;}, style: {} }),
|
|
documentElement: { getAttribute: () => null, setAttribute: () => {}, dataset: {} },
|
|
hidden: false,
|
|
},
|
|
console, Date, Infinity, Math, Array, Object, String, Number, JSON, RegExp,
|
|
Error, TypeError, Map, Set, Promise, URLSearchParams,
|
|
parseInt, parseFloat, isNaN, isFinite,
|
|
encodeURIComponent, decodeURIComponent,
|
|
setTimeout: () => 0, clearTimeout: () => {},
|
|
setInterval: () => 0, clearInterval: () => {},
|
|
fetch: () => Promise.resolve({ json: () => Promise.resolve({}) }),
|
|
performance: { now: () => Date.now() },
|
|
requestAnimationFrame: (cb) => 0,
|
|
cancelAnimationFrame: () => {},
|
|
localStorage: (() => {
|
|
const store = {};
|
|
return {
|
|
getItem: k => (k in store ? store[k] : null),
|
|
setItem: (k, v) => { store[k] = String(v); },
|
|
removeItem: k => { delete store[k]; },
|
|
};
|
|
})(),
|
|
location: { hash: '', protocol: 'https:', host: 'localhost' },
|
|
addEventListener: () => {},
|
|
dispatchEvent: () => {},
|
|
getComputedStyle: () => ({ getPropertyValue: () => '' }),
|
|
matchMedia: () => ({ matches: false, addEventListener: () => {} }),
|
|
navigator: {},
|
|
L: {
|
|
circleMarker: () => ({ addTo(){return this;}, bindTooltip(){return this;}, on(){return this;}, setRadius(){}, setStyle(){}, setLatLng(){}, getLatLng(){return {lat:0,lng:0};}, remove(){} }),
|
|
polyline: () => ({ addTo(){return this;}, setStyle(){}, remove(){} }),
|
|
polygon: () => ({ addTo(){return this;}, remove(){} }),
|
|
map: () => ({ setView(){return this;}, addLayer(){return this;}, on(){return this;}, getZoom(){return 11;}, getCenter(){return {lat:37,lng:-122};}, getBounds(){return {contains:()=>true};}, fitBounds(){return this;}, invalidateSize(){}, remove(){}, hasLayer(){return false;}, removeLayer(){} }),
|
|
layerGroup: () => ({ addTo(){return this;}, addLayer(){}, removeLayer(){}, clearLayers(){}, hasLayer(){return true;}, eachLayer(){} }),
|
|
tileLayer: () => ({ addTo(){return this;} }),
|
|
control: { attribution: () => ({ addTo(){} }) },
|
|
DomUtil: { addClass(){}, removeClass(){} },
|
|
},
|
|
registerPage: () => {}, onWS: () => {}, offWS: () => {}, connectWS: () => {},
|
|
api: () => Promise.resolve([]), invalidateApiCache: () => {},
|
|
favStar: () => '', bindFavStars: () => {},
|
|
getFavorites: () => [], isFavorite: () => false,
|
|
HopResolver: { init(){}, resolve: () => ({}), ready: () => false },
|
|
MeshAudio: null,
|
|
RegionFilter: { init(){}, getSelected: () => null, onRegionChange: () => {} },
|
|
CustomEvent: function (n, d) { this.type = n; this.detail = (d && d.detail) || null; },
|
|
module: { exports: {} }, exports: {},
|
|
};
|
|
ctx.window.localStorage = ctx.localStorage;
|
|
vm.createContext(ctx);
|
|
// Load filter helpers first so live.js sees window.MC_* on import.
|
|
load(ctx, 'public/payload-labels.js');
|
|
load(ctx, 'public/hop-filter.js');
|
|
// Mirror window.* onto top-level for files that reference bare names.
|
|
for (const k of Object.keys(ctx.window)) ctx[k] = ctx.window[k];
|
|
// packet-helpers / roles are required by live.js for getParsedPath etc.
|
|
try { load(ctx, 'public/roles.js'); } catch (_e) {}
|
|
try { load(ctx, 'public/packet-helpers.js'); } catch (_e) {}
|
|
for (const k of Object.keys(ctx.window)) ctx[k] = ctx.window[k];
|
|
try { load(ctx, 'public/live.js'); } catch (e) {
|
|
console.error('live.js load error:', e.message);
|
|
}
|
|
for (const k of Object.keys(ctx.window)) ctx[k] = ctx.window[k];
|
|
return ctx;
|
|
}
|
|
|
|
console.log('=== #1633: hide 1-byte path hops everywhere ===');
|
|
|
|
test('default is OFF (back-compat)', () => {
|
|
const ctx = freshSandbox();
|
|
assert.strictEqual(ctx.window.MC_getHide1ByteHops(), false, 'default must be OFF');
|
|
});
|
|
|
|
test('hopByteLen: "AB"=1, "ABCD"=2, "ABCDEF"=3, ""=0', () => {
|
|
const ctx = freshSandbox();
|
|
const bl = ctx.window.MC_hopByteLen;
|
|
assert.strictEqual(bl('AB'), 1);
|
|
assert.strictEqual(bl('ABCD'), 2);
|
|
assert.strictEqual(bl('ABCDEF'), 3);
|
|
assert.strictEqual(bl(''), 0);
|
|
assert.strictEqual(bl(null), 0);
|
|
});
|
|
|
|
test('isVisibleHop: toggle OFF → every hop visible', () => {
|
|
const ctx = freshSandbox();
|
|
const f = ctx.window.MC_isVisibleHop;
|
|
assert.strictEqual(f('AB', { hide1ByteHops: false }), true);
|
|
assert.strictEqual(f('CDEF', { hide1ByteHops: false }), true);
|
|
});
|
|
|
|
test('isVisibleHop: toggle ON → 1-byte HIDDEN, 2/3-byte SHOWN', () => {
|
|
const ctx = freshSandbox();
|
|
const f = ctx.window.MC_isVisibleHop;
|
|
assert.strictEqual(f('AB', { hide1ByteHops: true }), false, '1-byte must hide');
|
|
assert.strictEqual(f('CDEF', { hide1ByteHops: true }), true, '2-byte must stay');
|
|
assert.strictEqual(f('ABCDEF', { hide1ByteHops: true }), true, '3-byte must stay');
|
|
});
|
|
|
|
test('filterPathHops: mixed input → only multi-byte kept when ON', () => {
|
|
const ctx = freshSandbox();
|
|
const f = ctx.window.MC_filterPathHops;
|
|
const mixed = ['AB', 'CDEF', '12', 'ABCDEF', '34'];
|
|
// Use Array.from to bridge the vm-realm Array prototype gap.
|
|
assert.deepStrictEqual(
|
|
Array.from(f(mixed, { hide1ByteHops: true })),
|
|
['CDEF', 'ABCDEF'],
|
|
'must drop every 1-byte entry'
|
|
);
|
|
assert.deepStrictEqual(
|
|
Array.from(f(mixed, { hide1ByteHops: false })),
|
|
mixed,
|
|
'OFF must return input unchanged'
|
|
);
|
|
});
|
|
|
|
test('HopDisplay.renderPath rendered chips match filtered hops when ON', () => {
|
|
const ctx = freshSandbox();
|
|
// Apply the filter at the boundary the way every render site will.
|
|
const hops = ['AB', 'CDEF', '12', 'ABCDEF'];
|
|
const filtered = ctx.window.MC_filterPathHops(hops, { hide1ByteHops: true });
|
|
const html = ctx.window.HopDisplay.renderPath(filtered, {}, { hexMode: true, link: false });
|
|
// Filtered chip set: ONLY 2-byte + 3-byte tokens present.
|
|
assert.ok(html.indexOf('CDEF') !== -1, 'CDEF chip must render');
|
|
assert.ok(html.indexOf('ABCDEF') !== -1, 'ABCDEF chip must render');
|
|
// 1-byte hex tokens must NOT appear in the chip set. Use word
|
|
// boundaries with the chip wrapper to avoid matching substrings of
|
|
// the larger hops.
|
|
assert.strictEqual(html.indexOf('>AB<'), -1, '1-byte AB must NOT render as a chip');
|
|
assert.strictEqual(html.indexOf('>12<'), -1, '1-byte 12 must NOT render as a chip');
|
|
});
|
|
|
|
test('Map polyline source: positions[]._hopHex 1-byte dropped, origin/dest kept', () => {
|
|
const ctx = freshSandbox();
|
|
// Origin & destination have no _hopHex (came from payload, not from path bytes).
|
|
// Intermediate hops carry _hopHex tagged by drawPacketRoute.
|
|
const positions = [
|
|
{ name: 'Origin', isOrigin: true },
|
|
{ name: 'h1', _hopHex: 'AB' }, // 1-byte → filterable
|
|
{ name: 'h2', _hopHex: 'CDEF' }, // 2-byte → keep
|
|
{ name: 'h3', _hopHex: '12' }, // 1-byte → filterable
|
|
{ name: 'h4', _hopHex: 'ABCDEF' }, // 3-byte → keep
|
|
{ name: 'Dest', isDest: true }
|
|
];
|
|
const opts = { hide1ByteHops: true };
|
|
const kept = positions.filter(p =>
|
|
!p._hopHex || ctx.window.MC_isVisibleHop(p._hopHex, opts)
|
|
);
|
|
assert.deepStrictEqual(
|
|
kept.map(p => p.name),
|
|
['Origin', 'h2', 'h4', 'Dest'],
|
|
'polyline retains origin/dest + multi-byte hops only'
|
|
);
|
|
});
|
|
|
|
test('Analytics: route-pattern aggregation key drops 1-byte hops when ON', () => {
|
|
const ctx = freshSandbox();
|
|
// Each packet contributes one path. With hide ON, aggregation must key on
|
|
// the FILTERED hop list so 1-byte noise stops inflating distinct counts.
|
|
const packets = [
|
|
{ path_json: ['AB', 'CDEF', '12', 'ABCDEF'] },
|
|
{ path_json: ['99', 'CDEF', '88', 'ABCDEF'] }, // same multi-byte key
|
|
{ path_json: ['CDEF', 'ABCDEF'] } // identical w/o 1-byte
|
|
];
|
|
const opts = { hide1ByteHops: true };
|
|
const counts = {};
|
|
for (const p of packets) {
|
|
const key = ctx.window.MC_filterPathHops(p.path_json, opts).join('→');
|
|
counts[key] = (counts[key] || 0) + 1;
|
|
}
|
|
assert.deepStrictEqual(counts, { 'CDEF→ABCDEF': 3 },
|
|
'all three rows collapse to one pattern with 3 hits');
|
|
});
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
// #1689 r1 — additional regressions surfaced by adversarial + Kent Beck
|
|
// review on the original PR.
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
|
console.log('\n=== #1689 r1: review fix-ups (adv + kb findings) ===');
|
|
|
|
test('[kb #3] MC_filterPathHops handles null/undefined/empty without throwing', () => {
|
|
const ctx = freshSandbox();
|
|
const f = ctx.window.MC_filterPathHops;
|
|
assert.deepStrictEqual(Array.from(f([])), [], 'empty in → empty out');
|
|
// null/undefined inputs should NOT throw. The contract is "return a
|
|
// safe empty-ish array". Either [] or the input back is acceptable;
|
|
// we assert non-throwing + array-shape.
|
|
const a = f(null);
|
|
const b = f(undefined);
|
|
assert.ok(Array.isArray(a) || a == null, 'null → null/array');
|
|
assert.ok(Array.isArray(b) || b == null, 'undefined → null/array');
|
|
assert.deepStrictEqual(Array.from(f([], { hide1ByteHops: true })), [], 'empty in + ON → empty');
|
|
});
|
|
|
|
test('[kb #3] MC_isVisibleHop(null) is safe (does not throw; null treated as 0-byte → visible)', () => {
|
|
const ctx = freshSandbox();
|
|
const v = ctx.window.MC_isVisibleHop;
|
|
// null/undefined hops have byteLen 0, !== 1, so they are visible
|
|
// regardless of the toggle (origin/dest sentinels rely on this).
|
|
assert.strictEqual(v(null, { hide1ByteHops: true }), true, 'null hop must be visible');
|
|
assert.strictEqual(v(undefined, { hide1ByteHops: true }), true, 'undefined must be visible');
|
|
assert.strictEqual(v('', { hide1ByteHops: true }), true, 'empty string must be visible');
|
|
});
|
|
|
|
test('[adv #1] route-view.js: all-1-byte paths do NOT collide on empty key', () => {
|
|
// The bug: route-view aggregated `seen[hops.join('-')]`. When the filter
|
|
// produced an empty array, every distinct all-1-byte path collapsed onto
|
|
// key "". Distinct routes vanished into one bucket. Fix uses a sentinel
|
|
// prefixed with the rawHops signature so the buckets stay distinct.
|
|
//
|
|
// We assert the EXACT inline aggregation logic now used in route-view.js
|
|
// by replaying its key derivation on representative inputs.
|
|
const ctx = freshSandbox();
|
|
const aggregate = (paths) => {
|
|
const seen = {};
|
|
paths.forEach((p) => {
|
|
const rawHops = p.path || [];
|
|
const hops = ctx.window.MC_filterPathHops(rawHops);
|
|
const key = hops.length ? hops.join('-') : ('__all1byte__::' + rawHops.join('-'));
|
|
seen[key] = true;
|
|
});
|
|
return Object.keys(seen).length;
|
|
};
|
|
// Toggle ON
|
|
ctx.window.MC_setHide1ByteHops(true);
|
|
const distinct = aggregate([
|
|
{ path: ['AB', 'CD'] }, // all 1-byte route A
|
|
{ path: ['EF', 'GH'] }, // all 1-byte route B — must NOT collide with A
|
|
{ path: ['AB', 'CD'] } // duplicate of A — must collide with A
|
|
]);
|
|
assert.strictEqual(distinct, 2,
|
|
'two distinct all-1-byte routes + one dup must produce 2 buckets, not 1');
|
|
});
|
|
|
|
test('[adv #1] route-view.js source: key derivation uses sentinel for all-1-byte buckets', () => {
|
|
// Source-grep guard: if a future refactor reverts to `hops.join("-")`
|
|
// bare aggregation, this test catches it.
|
|
const src = fs.readFileSync(path.join(__dirname, 'public/route-view.js'), 'utf8');
|
|
assert.ok(src.indexOf('__all1byte__::') !== -1 || src.indexOf('⟨all-1byte⟩::') !== -1,
|
|
'route-view.js must include a sentinel for all-1-byte aggregation buckets');
|
|
});
|
|
|
|
test('[adv #2] map.js source: drawPacketRoute propagates hopsHiddenCount to renderer', () => {
|
|
// Source-grep guard: the polyline MUST carry the redacted-count down
|
|
// to the renderer so it can dash + badge. A revert that drops the
|
|
// `hopsHiddenCount` option from the render call regresses operator UX.
|
|
const src = fs.readFileSync(path.join(__dirname, 'public/map.js'), 'utf8');
|
|
assert.ok(/hopsHiddenCount\s*:\s*hopsHiddenCount/.test(src),
|
|
'map.js must pass hopsHiddenCount into the route renderer opts');
|
|
assert.ok(/var\s+hopsHiddenCount\s*=\s*0/.test(src) || /let\s+hopsHiddenCount\s*=\s*0/.test(src),
|
|
'map.js must compute hopsHiddenCount before filtering positions');
|
|
});
|
|
|
|
test('[adv #2] route-render.js: redacted edges use dashed + reduced opacity + emit badge', () => {
|
|
const src = fs.readFileSync(path.join(__dirname, 'public/route-render.js'), 'utf8');
|
|
assert.ok(src.indexOf('mc-route-edge-redacted') !== -1,
|
|
'route-render.js must add a .mc-route-edge-redacted className when hopsHiddenCount > 0');
|
|
assert.ok(src.indexOf('mc-route-redacted-badge') !== -1,
|
|
'route-render.js must emit a midpoint badge when hopsHiddenCount > 0');
|
|
assert.ok(/opacity:\s*redacted\s*\?/.test(src),
|
|
'route-render.js must lower opacity when redacted');
|
|
});
|
|
|
|
test('[adv #3] observer-detail.js source: hop count honors MC_filterPathHops', () => {
|
|
const src = fs.readFileSync(path.join(__dirname, 'public/observer-detail.js'), 'utf8');
|
|
assert.ok(src.indexOf('MC_filterPathHops') !== -1,
|
|
'observer-detail.js renderRecentPackets must call MC_filterPathHops');
|
|
});
|
|
|
|
test('[r2 MAJOR] live.js packetInvolvesFilterNode: node-filter search is INDEPENDENT of hide-1byte toggle', () => {
|
|
// r2 reviewer (option 1): drop the 1-byte hop filter from
|
|
// packetInvolvesFilterNode entirely. The node-filter search box must
|
|
// return the same matches regardless of the display preference; the
|
|
// chip rendering already has separate filtering via feedHops.
|
|
//
|
|
// Behavioral assertion via vm-sandbox load of live.js (replaces the
|
|
// prior source-grep tautology for this consumer).
|
|
const ctx = makeLiveSandbox();
|
|
const fn = ctx.window._livePacketInvolvesFilterNode;
|
|
assert.ok(fn, '_livePacketInvolvesFilterNode must be exposed');
|
|
|
|
// Filter-node appears ONLY in a 1-byte hop.
|
|
const pkt = { decoded: { path: { hops: ['ab'] }, payload: {} } };
|
|
const filterKeys = ['abcd1234567890ab'];
|
|
|
|
// Toggle OFF — must match (baseline).
|
|
ctx.window.MC_setHide1ByteHops(false);
|
|
assert.strictEqual(fn(pkt, filterKeys), true,
|
|
'baseline: match a filter-key whose prefix overlaps a 1-byte hop');
|
|
|
|
// Toggle ON — MUST STILL match. The node-filter search box is
|
|
// semantically independent of the display preference.
|
|
ctx.window.MC_setHide1ByteHops(true);
|
|
assert.strictEqual(fn(pkt, filterKeys), true,
|
|
'node-filter search must be independent of hide-1byte display toggle');
|
|
});
|
|
|
|
test('[adv #4] customize-v2.js still dispatches mc-hide-1byte-hops-changed', () => {
|
|
const src = fs.readFileSync(path.join(__dirname, 'public/customize-v2.js'), 'utf8');
|
|
assert.ok(src.indexOf('mc-hide-1byte-hops-changed') !== -1,
|
|
'customizer must continue to dispatch the event');
|
|
});
|
|
|
|
test('[adv #4] map.js + packets.js subscribe to mc-hide-1byte-hops-changed', () => {
|
|
const mapSrc = fs.readFileSync(path.join(__dirname, 'public/map.js'), 'utf8');
|
|
const pktSrc = fs.readFileSync(path.join(__dirname, 'public/packets.js'), 'utf8');
|
|
assert.ok(/addEventListener\(['"]mc-hide-1byte-hops-changed['"]/.test(mapSrc),
|
|
'map.js must subscribe to mc-hide-1byte-hops-changed');
|
|
assert.ok(/addEventListener\(['"]mc-hide-1byte-hops-changed['"]/.test(pktSrc),
|
|
'packets.js must subscribe to mc-hide-1byte-hops-changed');
|
|
});
|
|
|
|
test('[adv #4] hop-filter dispatches the event when toggled', () => {
|
|
const ctx = freshSandbox();
|
|
let saw = null;
|
|
ctx.window.dispatchEvent = function (ev) { saw = ev; };
|
|
ctx.window.MC_setHide1ByteHops(true);
|
|
assert.ok(saw, 'must dispatch CustomEvent on setHide1ByteHops');
|
|
assert.strictEqual(saw.type, 'mc-hide-1byte-hops-changed');
|
|
assert.strictEqual(saw.detail && saw.detail.value, true);
|
|
});
|
|
|
|
test('[kb #2] (1-byte filtered) chip text is emitted by every render boundary', () => {
|
|
// PR body promised the chip. Assert the literal text exists in every
|
|
// consumer file that renders a path. If a future refactor strips it,
|
|
// operators lose the only signal that the route was redacted.
|
|
const files = [
|
|
'public/packets.js',
|
|
'public/route-view.js'
|
|
];
|
|
for (const f of files) {
|
|
const src = fs.readFileSync(path.join(__dirname, f), 'utf8');
|
|
assert.ok(src.indexOf('1-byte filtered') !== -1,
|
|
f + ' must render the literal "(1-byte filtered)" chip text');
|
|
}
|
|
});
|
|
|
|
test('[kb #1] anti-tautology: tests reference the actual production files (not inline copies)', () => {
|
|
// This file is allowed ONLY a single inline filter recreation (the
|
|
// route-view aggregation guard). Every other consumer assertion must
|
|
// source-grep the production file, NOT re-implement the filter.
|
|
const selfSrc = fs.readFileSync(__filename, 'utf8');
|
|
// Ensure the test file does NOT define its own filterPathHops.
|
|
assert.strictEqual(
|
|
/function\s+filterPathHops\s*\(/.test(selfSrc), false,
|
|
'test file must not re-implement filterPathHops (tautology guard)'
|
|
);
|
|
});
|
|
|
|
console.log('\n' + passed + ' passed, ' + failed + ' failed');
|
|
process.exit(failed > 0 ? 1 : 0);
|