mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-09-26 23: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>
135 lines
6.1 KiB
JavaScript
135 lines
6.1 KiB
JavaScript
/* Unit test (#1136): live.js must parse /api/observers correctly.
|
|
*
|
|
* Regression: PR #1080 wrote `if (Array.isArray(list))` and treated the
|
|
* response as a top-level array. The actual /api/observers shape is
|
|
* `{ observers: [...], server_time: "..." }` (cmd/server/types.go
|
|
* ObserverListResponse). Result: observerIataMap stays empty and ANY
|
|
* region selection drops every packet.
|
|
*
|
|
* This test loads live.js into a vm sandbox and asserts that the exposed
|
|
* builder helper produces a populated map from the realistic API shape.
|
|
*/
|
|
'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(' \u2705 ' + name); }
|
|
catch (e) { failed++; console.log(' \u274C ' + name + ': ' + e.message); }
|
|
}
|
|
|
|
function makeSandbox() {
|
|
const ctx = {
|
|
window: { addEventListener: () => {}, dispatchEvent: () => {}, devicePixelRatio: 1 },
|
|
document: {
|
|
readyState: 'complete',
|
|
createElement: () => ({ style: {}, classList: { add(){}, remove(){}, contains(){return false;} }, setAttribute(){}, addEventListener(){}, getContext: () => ({clearRect(){},fillRect(){},beginPath(){},arc(){},fill(){},scale(){},fillText(){}}) }),
|
|
head: { appendChild: () => {} },
|
|
getElementById: () => null,
|
|
addEventListener: () => {},
|
|
querySelectorAll: () => [], querySelector: () => null,
|
|
createElementNS: () => ({ setAttribute(){} }),
|
|
documentElement: { getAttribute: () => null, setAttribute: () => {}, dataset: {} },
|
|
body: { appendChild: () => {}, removeChild: () => {}, contains: () => false },
|
|
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: () => 0,
|
|
cancelAnimationFrame: () => {},
|
|
localStorage: (() => { const s = {}; return { getItem: k => s[k] !== undefined ? s[k] : null, setItem: (k,v) => { s[k] = String(v); }, removeItem: k => { delete s[k]; } }; })(),
|
|
location: { hash: '', protocol: 'https:', host: 'localhost' },
|
|
CustomEvent: class CustomEvent {},
|
|
addEventListener: () => {}, dispatchEvent: () => {},
|
|
getComputedStyle: () => ({ getPropertyValue: () => '' }),
|
|
matchMedia: () => ({ matches: false, addEventListener: () => {} }),
|
|
navigator: {}, visualViewport: null,
|
|
MutationObserver: function() { this.observe=()=>{}; this.disconnect=()=>{}; },
|
|
WebSocket: function() { this.close=()=>{}; },
|
|
IATA_COORDS_GEO: {},
|
|
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:0,lng:0};},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, onChange: () => {}, offChange: () => {}, regionQueryString: () => '', getRegionParam: () => '' },
|
|
};
|
|
vm.createContext(ctx);
|
|
return ctx;
|
|
}
|
|
|
|
function load(ctx, file) {
|
|
vm.runInContext(fs.readFileSync(file, 'utf8'), ctx);
|
|
for (const k of Object.keys(ctx.window)) ctx[k] = ctx.window[k];
|
|
}
|
|
|
|
console.log('\n=== live.js: /api/observers parse (#1136) ===');
|
|
const ctx = makeSandbox();
|
|
load(ctx, 'public/payload-labels.js');
|
|
load(ctx, 'public/roles.js');
|
|
load(ctx, 'public/live.js');
|
|
|
|
const build = ctx.window._liveBuildObserverIataMap;
|
|
assert.ok(build, '_liveBuildObserverIataMap must be exposed (regression: missing parser helper)');
|
|
|
|
const realShape = {
|
|
observers: [
|
|
{ id: 'OBS1', iata: 'SJC', name: 'A' },
|
|
{ id: 'OBS2', iata: 'OAK', name: 'B' },
|
|
{ id: 'OBS3', iata: 'SFO', name: 'C' },
|
|
{ id: 'OBS4', iata: null, name: 'no-iata' },
|
|
],
|
|
server_time: '2026-05-07T00:00:00Z',
|
|
};
|
|
|
|
test('parses {observers:[...], server_time} response and populates map', () => {
|
|
const m = build(realShape);
|
|
assert.strictEqual(m.OBS1, 'SJC');
|
|
assert.strictEqual(m.OBS2, 'OAK');
|
|
assert.strictEqual(m.OBS3, 'SFO');
|
|
});
|
|
|
|
test('skips observers without iata', () => {
|
|
const m = build(realShape);
|
|
assert.ok(!('OBS4' in m), 'observers with null iata should not be in map');
|
|
});
|
|
|
|
test('returns empty map for null/undefined input', () => {
|
|
assert.strictEqual(Object.keys(build(null)).length, 0);
|
|
assert.strictEqual(Object.keys(build(undefined)).length, 0);
|
|
});
|
|
|
|
test('returns empty map when observers field is missing', () => {
|
|
assert.strictEqual(Object.keys(build({ server_time: 'x' })).length, 0);
|
|
});
|
|
|
|
test('back-compat: also accepts a top-level array (defensive)', () => {
|
|
// If the API shape ever changes back, don\'t silently break.
|
|
const m = build([{ id: 'X1', iata: 'LAX' }]);
|
|
assert.strictEqual(m.X1, 'LAX');
|
|
});
|
|
|
|
console.log('\n' + '='.repeat(40));
|
|
console.log(' observer iata map tests: ' + passed + ' passed, ' + failed + ' failed');
|
|
console.log('='.repeat(40) + '\n');
|
|
if (failed > 0) process.exit(1);
|