mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-08-28 05:14:27 +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>
This commit is contained in:
co-authored by
mc-bot
openclaw-bot
clawbot
clawbot
parent
4654ce3386
commit
b74a64ccfa
@@ -466,6 +466,7 @@ jobs:
|
||||
BASE_URL=http://localhost:13581 node test-issue-1273-qr-overlay-height-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
BASE_URL=http://localhost:13581 node test-issue-1281-location-row-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
BASE_URL=http://localhost:13581 node test-issue-1279-legend-p2-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
BASE_URL=http://localhost:13581 node test-issue-1799-label-vocab-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-home-coverage-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-path-inspector-coverage-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1206-resize-observer-leak-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
|
||||
+36
-2
@@ -583,18 +583,29 @@ func (s *Server) handleConfigTheme(w http.ResponseWriter, r *http.Request) {
|
||||
"surface3": "#2d2d50",
|
||||
"sectionBg": "#1e1e34",
|
||||
}, s.cfg.ThemeDark, theme.ThemeDark)
|
||||
// #1799 PR #1804 r1 item 6: REQUEST→REQ rename is a BREAKING change to
|
||||
// the /api/config/theme shape. Compat policy for >=1 release cycle:
|
||||
// - INCOMING: if an operator's config.json / theme.json carries the
|
||||
// legacy "REQUEST" key, normalise it to "REQ" before mergeMap so
|
||||
// the override wins the canonical slot.
|
||||
// - OUTGOING: dual-emit REQ AND REQUEST in the GET response so any
|
||||
// consumer still reading the legacy key keeps working.
|
||||
typeColors := mergeMap(map[string]interface{}{
|
||||
"ADVERT": "#22c55e",
|
||||
"GRP_TXT": "#3b82f6",
|
||||
"TXT_MSG": "#f59e0b",
|
||||
"ACK": "#6b7280",
|
||||
"REQUEST": "#a855f7",
|
||||
"REQ": "#a855f7",
|
||||
"RESPONSE": "#06b6d4",
|
||||
"TRACE": "#ec4899",
|
||||
"PATH": "#14b8a6",
|
||||
"ANON_REQ": "#f43f5e",
|
||||
"UNKNOWN": "#6b7280",
|
||||
}, s.cfg.TypeColors, theme.TypeColors)
|
||||
}, normaliseTypeColorsLegacyKeys(s.cfg.TypeColors), normaliseTypeColorsLegacyKeys(theme.TypeColors))
|
||||
// Dual-emit REQUEST = REQ for back-compat (drop after >=1 release cycle).
|
||||
if v, ok := typeColors["REQ"]; ok {
|
||||
typeColors["REQUEST"] = v
|
||||
}
|
||||
|
||||
defaultHome := map[string]interface{}{
|
||||
"heroTitle": "CoreScope",
|
||||
@@ -3101,6 +3112,29 @@ func queryInt(r *http.Request, key string, def int) int {
|
||||
return n
|
||||
}
|
||||
|
||||
// normaliseTypeColorsLegacyKeys returns a copy of `m` with the legacy
|
||||
// "REQUEST" key renamed to the canonical "REQ". If both keys are present
|
||||
// the canonical wins (caller already chose the new name explicitly).
|
||||
// #1799 PR #1804 r1 item 6: keeps stale operator config.json working
|
||||
// after the REQUEST→REQ rename for >=1 release cycle. Returns nil for
|
||||
// a nil input so mergeMap's nil-skip continues to work.
|
||||
func normaliseTypeColorsLegacyKeys(m map[string]interface{}) map[string]interface{} {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]interface{}, len(m))
|
||||
for k, v := range m {
|
||||
out[k] = v
|
||||
}
|
||||
if legacy, ok := out["REQUEST"]; ok {
|
||||
if _, hasCanon := out["REQ"]; !hasCanon {
|
||||
out["REQ"] = legacy
|
||||
}
|
||||
delete(out, "REQUEST")
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func mergeMap(base map[string]interface{}, overlays ...map[string]interface{}) map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
for k, v := range base {
|
||||
|
||||
@@ -4807,3 +4807,49 @@ func TestPostPacketPersistsV3Schema(t *testing.T) {
|
||||
t.Errorf("timestamp: want unix int near %d, got %d", nowSec, gotTS)
|
||||
}
|
||||
}
|
||||
|
||||
// TestConfigThemeTypeColorsLegacyRequestKey verifies the REQUEST→REQ rename
|
||||
// (#1799 PR #1804 r1 item 6) doesn't break operators whose config.json still
|
||||
// carries the legacy `typeColors.REQUEST` key. The GET response must:
|
||||
// - accept a config that supplies "REQUEST" and have that value win the
|
||||
// mergeMap precedence for the corresponding logical slot
|
||||
// - emit BOTH "REQ" and "REQUEST" in typeColors for ≥1 release cycle so
|
||||
// downstream consumers reading the legacy key keep working
|
||||
func TestConfigThemeTypeColorsLegacyRequestKey(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
seedTestData(t, db)
|
||||
cfg := &Config{
|
||||
Port: 3000,
|
||||
TypeColors: map[string]interface{}{
|
||||
// Operator's stale config — legacy key only.
|
||||
"REQUEST": "#deadbe",
|
||||
},
|
||||
}
|
||||
hub := NewHub()
|
||||
srv := NewServer(db, cfg, hub)
|
||||
router := mux.NewRouter()
|
||||
srv.RegisterRoutes(router)
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/config/theme", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
if w.Code != 200 {
|
||||
t.Fatalf("expected 200, got %d", w.Code)
|
||||
}
|
||||
var body map[string]interface{}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
tc, ok := body["typeColors"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("typeColors missing or wrong shape: %v", body["typeColors"])
|
||||
}
|
||||
// Legacy operator override must propagate to the canonical REQ slot.
|
||||
if tc["REQ"] != "#deadbe" {
|
||||
t.Errorf("typeColors.REQ: want #deadbe (from legacy REQUEST override), got %v", tc["REQ"])
|
||||
}
|
||||
// Back-compat emission: REQUEST also present, equal to REQ.
|
||||
if tc["REQUEST"] != "#deadbe" {
|
||||
t.Errorf("typeColors.REQUEST: want #deadbe (back-compat dual-emit), got %v", tc["REQUEST"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ Affects map markers, packet path badges, node lists, and legends.
|
||||
"GRP_TXT": "#3b82f6",
|
||||
"TXT_MSG": "#f59e0b",
|
||||
"ACK": "#6b7280",
|
||||
"REQUEST": "#a855f7",
|
||||
"REQ": "#a855f7",
|
||||
"RESPONSE": "#06b6d4",
|
||||
"TRACE": "#ec4899",
|
||||
"PATH": "#14b8a6",
|
||||
|
||||
@@ -119,7 +119,7 @@ The user override delta is a sparse object — it only contains fields the user
|
||||
"GRP_TXT": "string — CSS color",
|
||||
"TXT_MSG": "string — CSS color",
|
||||
"ACK": "string — CSS color",
|
||||
"REQUEST": "string — CSS color",
|
||||
"REQ": "string — CSS color",
|
||||
"RESPONSE": "string — CSS color",
|
||||
"TRACE": "string — CSS color",
|
||||
"PATH": "string — CSS color",
|
||||
|
||||
+32
-10
@@ -245,7 +245,7 @@
|
||||
nodeColors: { repeater: '#ff0000', companion: '#0066ff', room: '#009900', sensor: '#cc8800', observer: '#9933ff' },
|
||||
typeColors: {
|
||||
ADVERT: '#009900', GRP_TXT: '#0066ff', TXT_MSG: '#cc8800', ACK: '#666666',
|
||||
REQUEST: '#9933ff', RESPONSE: '#0099cc', TRACE: '#cc0066', PATH: '#009999', ANON_REQ: '#cc3355'
|
||||
REQ: '#9933ff', RESPONSE: '#0099cc', TRACE: '#cc0066', PATH: '#009999', ANON_REQ: '#cc3355'
|
||||
}
|
||||
},
|
||||
midnight: {
|
||||
@@ -328,19 +328,35 @@
|
||||
};
|
||||
var NODE_EMOJI = { repeater: 'ph:diamond', companion: 'ph:circle-fill', room: 'ph:square-fill', sensor: 'ph:triangle', observer: 'ph:star-fill' };
|
||||
|
||||
var TYPE_LABELS = {
|
||||
ADVERT: 'Advertisement', GRP_TXT: 'Channel Message', TXT_MSG: 'Direct Message', ACK: 'Acknowledgment',
|
||||
REQUEST: 'Request', RESPONSE: 'Response', TRACE: 'Traceroute', PATH: 'Path', ANON_REQ: 'Anonymous Request'
|
||||
};
|
||||
// PR #1804 r1 item 3 (tufte3): consume canonical PayloadLabels shorts so
|
||||
// the v2 customizer matches every other surface. Defensive literal
|
||||
// fallback mirrors packets.js.
|
||||
var TYPE_LABELS = (function () {
|
||||
var FALLBACK = {
|
||||
ADVERT: 'Advert', GRP_TXT: 'Channel Msg', TXT_MSG: 'Direct Msg', ACK: 'ACK',
|
||||
REQ: 'Request', RESPONSE: 'Response', TRACE: 'Trace', PATH: 'Path',
|
||||
ANON_REQ: 'Anon Req'
|
||||
};
|
||||
var PL = window.PayloadLabels;
|
||||
if (!PL || !PL.SHORT_BY_ID) {
|
||||
console.error('customize-v2.js: window.PayloadLabels missing — using inline TYPE_LABELS fallback.');
|
||||
return FALLBACK;
|
||||
}
|
||||
var out = {};
|
||||
for (var k in FALLBACK) {
|
||||
out[k] = (PL[k] && PL[k].short) || FALLBACK[k];
|
||||
}
|
||||
return out;
|
||||
})();
|
||||
var TYPE_HINTS = {
|
||||
ADVERT: 'Node advertisements', GRP_TXT: 'Group/channel messages', TXT_MSG: 'Direct messages',
|
||||
ACK: 'Acknowledgments', REQUEST: 'Requests', RESPONSE: 'Responses',
|
||||
ACK: 'Acknowledgments', REQ: 'Requests', RESPONSE: 'Responses',
|
||||
TRACE: 'Traceroute', PATH: 'Path packets', ANON_REQ: 'Encrypted anonymous requests'
|
||||
};
|
||||
// #1648 M5: defaults use 'ph:<name>' tokens. renderConfigGlyph() below
|
||||
// accepts both new ph tokens AND legacy emoji strings (back-compat).
|
||||
var TYPE_EMOJI = {
|
||||
ADVERT: 'ph:broadcast', GRP_TXT: 'ph:chat-circle', TXT_MSG: 'ph:envelope', ACK: 'ph:check', REQUEST: 'ph:question',
|
||||
ADVERT: 'ph:broadcast', GRP_TXT: 'ph:chat-circle', TXT_MSG: 'ph:envelope', ACK: 'ph:check', REQ: 'ph:question',
|
||||
RESPONSE: 'ph:envelope-simple', TRACE: 'ph:magnifying-glass', PATH: 'ph:path', ANON_REQ: 'ph:lock'
|
||||
};
|
||||
|
||||
@@ -710,8 +726,12 @@
|
||||
var tc = effectiveConfig.typeColors;
|
||||
if (tc) {
|
||||
for (var type in tc) {
|
||||
root.setProperty('--type-' + type.toLowerCase(), tc[type]);
|
||||
if (window.TYPE_COLORS && type in window.TYPE_COLORS) window.TYPE_COLORS[type] = tc[type];
|
||||
// #1799 r1 item 6: legacy stored configs use 'REQUEST'; canonical
|
||||
// key is 'REQ'. Migrate at read time so operator color choices
|
||||
// survive the rename.
|
||||
var canon = (type === 'REQUEST') ? 'REQ' : type;
|
||||
root.setProperty('--type-' + canon.toLowerCase(), tc[type]);
|
||||
if (window.TYPE_COLORS && canon in window.TYPE_COLORS) window.TYPE_COLORS[canon] = tc[type];
|
||||
}
|
||||
if (window.syncBadgeColors) window.syncBadgeColors();
|
||||
}
|
||||
@@ -2699,7 +2719,9 @@
|
||||
}
|
||||
if (earlyOverrides.typeColors && window.TYPE_COLORS) {
|
||||
for (var type in earlyOverrides.typeColors) {
|
||||
if (type in window.TYPE_COLORS) window.TYPE_COLORS[type] = earlyOverrides.typeColors[type];
|
||||
// #1799 r1 item 6: REQUEST → REQ migration (see comment above).
|
||||
var canon = (type === 'REQUEST') ? 'REQ' : type;
|
||||
if (canon in window.TYPE_COLORS) window.TYPE_COLORS[canon] = earlyOverrides.typeColors[type];
|
||||
}
|
||||
if (window.syncBadgeColors) window.syncBadgeColors();
|
||||
}
|
||||
|
||||
+25
-9
@@ -75,7 +75,7 @@
|
||||
},
|
||||
typeColors: {
|
||||
ADVERT: '#22c55e', GRP_TXT: '#3b82f6', TXT_MSG: '#f59e0b', ACK: '#6b7280',
|
||||
REQUEST: '#a855f7', RESPONSE: '#06b6d4', TRACE: '#ec4899', PATH: '#14b8a6',
|
||||
REQ: '#a855f7', RESPONSE: '#06b6d4', TRACE: '#ec4899', PATH: '#14b8a6',
|
||||
ANON_REQ: '#f43f5e'
|
||||
},
|
||||
home: {
|
||||
@@ -259,7 +259,7 @@
|
||||
nodeColors: { repeater: '#ff0000', companion: '#0066ff', room: '#009900', sensor: '#cc8800', observer: '#9933ff' },
|
||||
typeColors: {
|
||||
ADVERT: '#009900', GRP_TXT: '#0066ff', TXT_MSG: '#cc8800', ACK: '#666666',
|
||||
REQUEST: '#9933ff', RESPONSE: '#0099cc', TRACE: '#cc0066', PATH: '#009999', ANON_REQ: '#cc3355'
|
||||
REQ: '#9933ff', RESPONSE: '#0099cc', TRACE: '#cc0066', PATH: '#009999', ANON_REQ: '#cc3355'
|
||||
}
|
||||
},
|
||||
midnight: {
|
||||
@@ -461,17 +461,33 @@
|
||||
// renderConfigGlyph() for operators with stored config (back-compat).
|
||||
const NODE_EMOJI = { repeater: 'ph:diamond', companion: 'ph:circle-fill', room: 'ph:square-fill', sensor: 'ph:triangle', observer: 'ph:star-fill' };
|
||||
|
||||
const TYPE_LABELS = {
|
||||
ADVERT: 'Advertisement', GRP_TXT: 'Channel Message', TXT_MSG: 'Direct Message', ACK: 'Acknowledgment',
|
||||
REQUEST: 'Request', RESPONSE: 'Response', TRACE: 'Traceroute', PATH: 'Path',
|
||||
ANON_REQ: 'Anonymous Request'
|
||||
};
|
||||
// PR #1804 r1 item 3 (tufte3): TYPE_LABELS now consumes the canonical
|
||||
// PayloadLabels short labels so the customizer matches every other
|
||||
// surface. Defensive literal fallback mirrors the policy used in
|
||||
// packets.js — drift gate keeps these byte-equal to canonical.
|
||||
const TYPE_LABELS = (function () {
|
||||
const FALLBACK = {
|
||||
ADVERT: 'Advert', GRP_TXT: 'Channel Msg', TXT_MSG: 'Direct Msg', ACK: 'ACK',
|
||||
REQ: 'Request', RESPONSE: 'Response', TRACE: 'Trace', PATH: 'Path',
|
||||
ANON_REQ: 'Anon Req'
|
||||
};
|
||||
const PL = window.PayloadLabels;
|
||||
if (!PL || !PL.SHORT_BY_ID) {
|
||||
console.error('customize.js: window.PayloadLabels missing — using inline TYPE_LABELS fallback.');
|
||||
return FALLBACK;
|
||||
}
|
||||
const out = {};
|
||||
for (const k of Object.keys(FALLBACK)) {
|
||||
out[k] = (PL[k] && PL[k].short) || FALLBACK[k];
|
||||
}
|
||||
return out;
|
||||
})();
|
||||
const TYPE_HINTS = {
|
||||
ADVERT: 'Node advertisements — map, feed, packet list',
|
||||
GRP_TXT: 'Group/channel messages — map, feed, channels',
|
||||
TXT_MSG: 'Direct messages — map, feed',
|
||||
ACK: 'Acknowledgments — packet list',
|
||||
REQUEST: 'Requests — packet list, feed',
|
||||
REQ: 'Requests — packet list, feed',
|
||||
RESPONSE: 'Responses — packet list',
|
||||
TRACE: 'Traceroute — map, traces page',
|
||||
PATH: 'Path packets — packet list',
|
||||
@@ -479,7 +495,7 @@
|
||||
};
|
||||
// #1648 M5: defaults are ph:<name>; renderConfigGlyph handles legacy emoji.
|
||||
const TYPE_EMOJI = {
|
||||
ADVERT: 'ph:broadcast', GRP_TXT: 'ph:chat-circle', TXT_MSG: 'ph:envelope', ACK: 'ph:check', REQUEST: 'ph:question', RESPONSE: 'ph:envelope-simple', TRACE: 'ph:magnifying-glass', PATH: 'ph:path', ANON_REQ: 'ph:lock'
|
||||
ADVERT: 'ph:broadcast', GRP_TXT: 'ph:chat-circle', TXT_MSG: 'ph:envelope', ACK: 'ph:check', REQ: 'ph:question', RESPONSE: 'ph:envelope-simple', TRACE: 'ph:magnifying-glass', PATH: 'ph:path', ANON_REQ: 'ph:lock'
|
||||
};
|
||||
|
||||
// renderConfigGlyph(value): given an operator-customizable config string,
|
||||
|
||||
@@ -166,6 +166,7 @@
|
||||
<main id="app" role="main"></main>
|
||||
|
||||
<script src="vendor/qrcode.js"></script>
|
||||
<script src="payload-labels.js?v=__BUST__"></script>
|
||||
<script src="roles.js?v=__BUST__"></script>
|
||||
<script src="map-tile-providers.js?v=__BUST__"></script>
|
||||
<script src="cb-presets.js?v=__BUST__"></script>
|
||||
|
||||
+35
-15
@@ -1,6 +1,14 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// #1799 PR #1804 r1 item 9 (adv6): payload-labels.js is loaded
|
||||
// synchronously before this script in index.html. Missing module is
|
||||
// a packaging bug — crash loud rather than render with stale inline
|
||||
// fallback data.
|
||||
if (!window.PayloadLabels) {
|
||||
throw new Error('live.js: window.PayloadLabels missing — payload-labels.js failed to load');
|
||||
}
|
||||
|
||||
// getParsedPath / getParsedDecoded are in shared packet-helpers.js (loaded before this file)
|
||||
var getParsedPath = window.getParsedPath;
|
||||
var getParsedDecoded = window.getParsedDecoded;
|
||||
@@ -152,14 +160,38 @@
|
||||
|
||||
const TYPE_COLORS = window.TYPE_COLORS || {
|
||||
ADVERT: '#22c55e', GRP_TXT: '#3b82f6', TXT_MSG: '#f59e0b', ACK: '#6b7280',
|
||||
REQUEST: '#a855f7', RESPONSE: '#06b6d4', TRACE: '#ec4899', PATH: '#14b8a6',
|
||||
REQ: '#a855f7', RESPONSE: '#06b6d4', TRACE: '#ec4899', PATH: '#14b8a6',
|
||||
ANON_REQ: '#f43f5e', GRP_DATA: '#8b5cf6', MULTIPART: '#0d9488',
|
||||
CONTROL: '#b45309', RAW_CUSTOM: '#c026d3'
|
||||
};
|
||||
|
||||
// #1804 r1 item 7 (adv3): legend builder extracted from the live-overlay
|
||||
// template IIFE so it is testable in isolation. See
|
||||
// test-live-legend-helper.js. Emits one <li data-enum="<ENUM>">…</li>
|
||||
// per entry in ORDER, each row formatted as `SHORT — LONG`.
|
||||
//
|
||||
// #1804 r1 item 9 (adv6): inline fallback dropped. The top-of-file
|
||||
// guard already crashed if PayloadLabels was missing, so by the time
|
||||
// we reach here the canonical map is guaranteed to be there.
|
||||
function buildLegendHtml(PL) {
|
||||
var src = (PL && PL.api) ? PL.api : PL;
|
||||
var enums = (PL && PL.enums) ? PL.enums : PL;
|
||||
var order = src.ORDER;
|
||||
return order.map(function (k) {
|
||||
var e = enums[k];
|
||||
if (!e) return '';
|
||||
var color = TYPE_COLORS[k] || '#888';
|
||||
var label = e.short + ' \u2014 ' + e.long;
|
||||
return '<li data-enum="' + k + '"><span class="live-dot" style="background:' + color + '" aria-hidden="true"></span> ' + label + '</li>';
|
||||
}).join('');
|
||||
}
|
||||
// Expose for tests + downstream consumers. The unit test reads this
|
||||
// via vm; in-page consumers can pull it off window for debugging.
|
||||
if (typeof window !== 'undefined') { window.buildLegendHtml = buildLegendHtml; }
|
||||
|
||||
const PAYLOAD_ICONS = {
|
||||
ADVERT: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-broadcast"/></svg>', GRP_TXT: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-chat-circle"/></svg>', TXT_MSG: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-envelope"/></svg>', ACK: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-check"/></svg>',
|
||||
REQUEST: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-question"/></svg>', RESPONSE: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-envelope"/></svg>', TRACE: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-magnifying-glass"/></svg>', PATH: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-path"/></svg>'
|
||||
REQ: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-question"/></svg>', RESPONSE: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-envelope"/></svg>', TRACE: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-magnifying-glass"/></svg>', PATH: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-path"/></svg>'
|
||||
};
|
||||
|
||||
/* ---- Panel Corner Positioning (#608 M0) ---- */
|
||||
@@ -1167,19 +1199,7 @@
|
||||
<div class="panel-content">
|
||||
<h3 class="legend-title">PACKET TYPES</h3>
|
||||
<ul class="legend-list">
|
||||
<li><span class="live-dot" style="background:${TYPE_COLORS.ADVERT}" aria-hidden="true"></span> Advert — Node advertisement</li>
|
||||
<li><span class="live-dot" style="background:${TYPE_COLORS.GRP_TXT}" aria-hidden="true"></span> Message — Group text</li>
|
||||
<li><span class="live-dot" style="background:${TYPE_COLORS.TXT_MSG}" aria-hidden="true"></span> Direct — Direct message</li>
|
||||
<li><span class="live-dot" style="background:${TYPE_COLORS.REQUEST}" aria-hidden="true"></span> Request — Data request</li>
|
||||
<li><span class="live-dot" style="background:${TYPE_COLORS.RESPONSE}" aria-hidden="true"></span> Response — Data response</li>
|
||||
<li><span class="live-dot" style="background:${TYPE_COLORS.TRACE}" aria-hidden="true"></span> Trace — Route trace</li>
|
||||
<li><span class="live-dot" style="background:${TYPE_COLORS.PATH}" aria-hidden="true"></span> Path — Path discovery</li>
|
||||
<li><span class="live-dot" style="background:${TYPE_COLORS.ANON_REQ}" aria-hidden="true"></span> Anon Req — Anonymous request</li>
|
||||
<li><span class="live-dot" style="background:${TYPE_COLORS.GRP_DATA}" aria-hidden="true"></span> Group Data — Group datagram</li>
|
||||
<li><span class="live-dot" style="background:${TYPE_COLORS.MULTIPART}" aria-hidden="true"></span> Multipart — Multi-fragment payload</li>
|
||||
<li><span class="live-dot" style="background:${TYPE_COLORS.CONTROL}" aria-hidden="true"></span> Control — Control plane</li>
|
||||
<li><span class="live-dot" style="background:${TYPE_COLORS.RAW_CUSTOM}" aria-hidden="true"></span> Raw Custom — Application-defined payload</li>
|
||||
<li><span class="live-dot" style="background:${TYPE_COLORS.ACK}" aria-hidden="true"></span> Ack / Other — Acknowledgment or unknown type</li>
|
||||
${buildLegendHtml(window.PayloadLabels || null)}
|
||||
</ul>
|
||||
<h3 class="legend-title" style="margin-top:8px">NODE ROLES</h3>
|
||||
<ul class="legend-list" id="roleLegendList"></ul>
|
||||
|
||||
+20
-5
@@ -4,11 +4,26 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Local copies of type maps (also available as window globals from app.js)
|
||||
// Standard firmware payload type names (canonical)
|
||||
var FW_PAYLOAD_TYPES = { 0: 'REQ', 1: 'RESPONSE', 2: 'TXT_MSG', 3: 'ACK', 4: 'ADVERT', 5: 'GRP_TXT', 6: 'GRP_DATA', 7: 'ANON_REQ', 8: 'PATH', 9: 'TRACE', 10: 'MULTIPART', 11: 'CONTROL', 15: 'RAW_CUSTOM' };
|
||||
// Aliases: display names → firmware names (for user convenience)
|
||||
var TYPE_ALIASES = { 'request': 'REQ', 'response': 'RESPONSE', 'direct msg': 'TXT_MSG', 'dm': 'TXT_MSG', 'ack': 'ACK', 'advert': 'ADVERT', 'channel msg': 'GRP_TXT', 'channel': 'GRP_TXT', 'group data': 'GRP_DATA', 'anon req': 'ANON_REQ', 'path': 'PATH', 'trace': 'TRACE', 'multipart': 'MULTIPART', 'control': 'CONTROL', 'raw': 'RAW_CUSTOM', 'custom': 'RAW_CUSTOM' };
|
||||
// Canonical payload-label module is the single source of truth (#1799).
|
||||
//
|
||||
// #1799 PR #1804 r1 item 9 (adv6): inline fallbacks dropped. In the
|
||||
// browser, payload-labels.js is loaded synchronously before this
|
||||
// script in index.html — a missing module is a packaging bug that
|
||||
// should crash loud, not silently mask itself as stale labels.
|
||||
var _PL;
|
||||
if (typeof window !== 'undefined') {
|
||||
if (!window.PayloadLabels) {
|
||||
throw new Error('packet-filter.js: window.PayloadLabels missing — payload-labels.js failed to load');
|
||||
}
|
||||
_PL = window.PayloadLabels;
|
||||
} else {
|
||||
_PL = require('./payload-labels.js');
|
||||
}
|
||||
// Prefer the .api namespace introduced in PR #1804 r1 item 8; legacy
|
||||
// root-level properties remain as a fall-through for older callers.
|
||||
var _src = _PL.api || _PL;
|
||||
var FW_PAYLOAD_TYPES = _src.FW_PAYLOAD_TYPES;
|
||||
var TYPE_ALIASES = _src.TYPE_ALIASES;
|
||||
var ROUTE_TYPES = { 0: 'TRANSPORT_FLOOD', 1: 'FLOOD', 2: 'DIRECT', 3: 'TRANSPORT_DIRECT' };
|
||||
// Aliases: shorthand → canonical route name (issue #339)
|
||||
var ROUTE_ALIASES = { 't_flood': 'TRANSPORT_FLOOD', 't_direct': 'TRANSPORT_DIRECT' };
|
||||
|
||||
+12
-2
@@ -623,6 +623,16 @@
|
||||
let packets = [];
|
||||
let hashIndex = new Map(); // hash → packet group for O(1) dedup
|
||||
|
||||
// #1799 PR #1804 r1 item 9 (adv6): payload-labels.js is loaded
|
||||
// synchronously before this script in index.html. A missing module
|
||||
// is a packaging bug, not a runtime degradation — fail loud.
|
||||
if (typeof window !== 'undefined' && !window.PayloadLabels) {
|
||||
throw new Error('packets.js: window.PayloadLabels missing — payload-labels.js failed to load');
|
||||
}
|
||||
const SHORT_BY_ID = window.PayloadLabels.api
|
||||
? window.PayloadLabels.api.SHORT_BY_ID
|
||||
: window.PayloadLabels.SHORT_BY_ID;
|
||||
|
||||
// Resolve observer_id to friendly name from loaded observers list
|
||||
function obsName(id) {
|
||||
if (!id) return '—';
|
||||
@@ -717,7 +727,7 @@
|
||||
// already built (decouples observer fetch from row render).
|
||||
let _rebuildObserverMenu = null;
|
||||
let regionMap = {};
|
||||
const TYPE_NAMES = { 0:'Request', 1:'Response', 2:'Direct Msg', 3:'ACK', 4:'Advert', 5:'Channel Msg', 6:'Group Data', 7:'Anon Req', 8:'Path', 9:'Trace', 10:'Multipart', 11:'Control', 15:'Raw Custom' };
|
||||
const TYPE_NAMES = SHORT_BY_ID;
|
||||
function typeName(t) { return TYPE_NAMES[t] ?? `Type ${t}`; }
|
||||
const isMobile = window.innerWidth <= 1024;
|
||||
const PACKET_LIMIT = isMobile ? 1000 : 50000;
|
||||
@@ -1745,7 +1755,7 @@
|
||||
// --- Type multi-select ---
|
||||
const typeMenu = document.getElementById('typeMenu');
|
||||
const typeTrigger = document.getElementById('typeTrigger');
|
||||
const typeMap = {0:'Request',1:'Response',2:'Direct Msg',3:'ACK',4:'Advert',5:'Channel Msg',6:'Group Data',7:'Anon Req',8:'Path',9:'Trace',10:'Multipart',11:'Control',15:'Raw Custom'};
|
||||
const typeMap = SHORT_BY_ID;
|
||||
const selectedTypes = new Set(filters.type ? String(filters.type).split(',') : []);
|
||||
function buildTypeMenu() {
|
||||
const allChecked = selectedTypes.size === 0;
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
/* payload-labels.js — canonical MeshCore payload-type label map.
|
||||
*
|
||||
* Single source of truth for human-readable labels of firmware payload type
|
||||
* enums. Surfaces that previously hand-rolled their own vocabularies
|
||||
* (packets.js typeMap, packet-filter.js FW_PAYLOAD_TYPES/TYPE_ALIASES,
|
||||
* live.js legend) consume this map so the same payload reads identically
|
||||
* everywhere the operator sees it.
|
||||
*
|
||||
* ABBREVIATION POLICY (#1799 PR #1804 r1 item 5):
|
||||
* - `short` is the compact label that appears in dropdowns, table cells,
|
||||
* legend titles, badges. Policy is <=12 characters, Title Case, with
|
||||
* ONE documented exception: 'ACK' (wire-protocol acronym). Abbreviate
|
||||
* only when the un-abbreviated form would exceed the cap or would be
|
||||
* ambiguous (e.g. 'Direct Msg' for 'Direct Message'). Apply the same
|
||||
* contraction rule across all entries — do not mix 'Direct Msg' with
|
||||
* 'Channel Message'.
|
||||
* - `long` is the descriptive sentence used in tooltips and legend
|
||||
* sub-text. It MUST describe the packet's purpose/behaviour, NOT
|
||||
* echo the short label. Reviewers reject `Path — Path discovery`-
|
||||
* shaped tautologies.
|
||||
* - When adding a new enum: pick a short that obeys the policy, then
|
||||
* write a long that a new operator could read aloud and learn what
|
||||
* the packet is for.
|
||||
*
|
||||
* Keyed by the firmware enum name; values carry:
|
||||
* enumName — firmware enum name (mirrors the key; #1799 r1 item 9)
|
||||
* short — compact label used in dropdowns, table cells, legend titles
|
||||
* long — descriptive label used in tooltips and legend sub-text
|
||||
* enumId — numeric firmware payload_type value (the wire byte)
|
||||
*
|
||||
* Refs #1799.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
// PAYLOAD_LABELS — every entry carries enumName for shape uniformity with
|
||||
// BY_ID (#1799 r1 item 9). `long` values are behavioural descriptions
|
||||
// (PR #1804 r1 item 2 / tufte2). Source for semantics: cmd/server/
|
||||
// decoder.go + cmd/ingestor/decoder.go + firmware Packet.h enum.
|
||||
var PAYLOAD_LABELS = {
|
||||
REQ: { enumName: 'REQ', short: 'Request', long: 'Encrypted data request to a remote node', enumId: 0 },
|
||||
RESPONSE: { enumName: 'RESPONSE', short: 'Response', long: 'Encrypted data response from a remote node', enumId: 1 },
|
||||
TXT_MSG: { enumName: 'TXT_MSG', short: 'Direct Msg', long: 'Encrypted point-to-point text message', enumId: 2 },
|
||||
ACK: { enumName: 'ACK', short: 'ACK', long: 'Acknowledgment of a prior message or request', enumId: 3 },
|
||||
ADVERT: { enumName: 'ADVERT', short: 'Advert', long: 'Node identity and capability advertisement', enumId: 4 },
|
||||
GRP_TXT: { enumName: 'GRP_TXT', short: 'Channel Msg', long: 'Channel-scoped group text message', enumId: 5 },
|
||||
GRP_DATA: { enumName: 'GRP_DATA', short: 'Group Data', long: 'Channel-scoped group datagram (non-text payload)', enumId: 6 },
|
||||
ANON_REQ: { enumName: 'ANON_REQ', short: 'Anon Req', long: 'Anonymous encrypted request via ephemeral key', enumId: 7 },
|
||||
PATH: { enumName: 'PATH', short: 'Path', long: 'Network path discovery and return-path advertisement', enumId: 8 },
|
||||
TRACE: { enumName: 'TRACE', short: 'Trace', long: 'Per-hop route trace with SNR samples', enumId: 9 },
|
||||
MULTIPART: { enumName: 'MULTIPART', short: 'Multipart', long: 'Fragmented payload reassembled across multiple packets', enumId: 10 },
|
||||
CONTROL: { enumName: 'CONTROL', short: 'Control', long: 'Mesh control-plane signalling (e.g. zero-hop direct)', enumId: 11 },
|
||||
RAW_CUSTOM: { enumName: 'RAW_CUSTOM', short: 'Raw Custom', long: 'Application-defined raw payload, no firmware envelope', enumId: 15 }
|
||||
};
|
||||
|
||||
// Legend display order (#1799 r1 item 5) — keeps Advert/GRP_TXT/TXT_MSG up
|
||||
// top per the historical Live legend layout.
|
||||
var ORDER = [
|
||||
'ADVERT', 'GRP_TXT', 'TXT_MSG', 'REQ', 'RESPONSE', 'TRACE', 'PATH',
|
||||
'ANON_REQ', 'GRP_DATA', 'MULTIPART', 'CONTROL', 'RAW_CUSTOM', 'ACK'
|
||||
];
|
||||
|
||||
// Reverse lookup: numeric enumId → entry. Built from PAYLOAD_LABELS.
|
||||
// Single `Object.entries` loop pattern across the module (#1799 r1 item 8).
|
||||
var BY_ID = {};
|
||||
Object.entries(PAYLOAD_LABELS).forEach(function (kv) {
|
||||
BY_ID[kv[1].enumId] = kv[1];
|
||||
});
|
||||
|
||||
// Numeric id → firmware enum name. Mirrors what packet-filter.js used to
|
||||
// hand-roll as FW_PAYLOAD_TYPES.
|
||||
var FW_PAYLOAD_TYPES = {};
|
||||
Object.entries(BY_ID).forEach(function (kv) {
|
||||
FW_PAYLOAD_TYPES[kv[0]] = kv[1].enumName;
|
||||
});
|
||||
|
||||
// Numeric id → short prose label. Replaces packets.js typeMap/TYPE_NAMES.
|
||||
var SHORT_BY_ID = {};
|
||||
Object.entries(BY_ID).forEach(function (kv) {
|
||||
SHORT_BY_ID[kv[0]] = kv[1].short;
|
||||
});
|
||||
|
||||
// User-input alias map (lowercased short label & legacy aliases) → enum.
|
||||
// Replaces packet-filter.js TYPE_ALIASES while staying backward-compatible
|
||||
// with the legacy free-text inputs operators have memorised.
|
||||
//
|
||||
// NOTE: 'raw custom' is intentionally listed alongside 'raw'/'custom' so
|
||||
// the documented filter syntax mirrors the rendered short label
|
||||
// ("Raw Custom"). (#1799 r1 item 11.)
|
||||
var TYPE_ALIASES = {
|
||||
'request': 'REQ',
|
||||
'response': 'RESPONSE',
|
||||
'direct msg': 'TXT_MSG',
|
||||
'dm': 'TXT_MSG',
|
||||
'ack': 'ACK',
|
||||
'advert': 'ADVERT',
|
||||
'channel msg': 'GRP_TXT',
|
||||
'channel': 'GRP_TXT',
|
||||
'group data': 'GRP_DATA',
|
||||
'anon req': 'ANON_REQ',
|
||||
'path': 'PATH',
|
||||
'trace': 'TRACE',
|
||||
'multipart': 'MULTIPART',
|
||||
'control': 'CONTROL',
|
||||
'raw': 'RAW_CUSTOM',
|
||||
'custom': 'RAW_CUSTOM',
|
||||
'raw custom': 'RAW_CUSTOM'
|
||||
};
|
||||
|
||||
// Public namespace (#1799 PR #1804 r1 item 8 / adv4): separate the
|
||||
// CANONICAL ENUM MAP from the HELPERS/DERIVED collections.
|
||||
// window.PayloadLabels.enums[ENUM] → entry { enumName, short, long, enumId }
|
||||
// window.PayloadLabels.api → helpers + derived (SHORT_BY_ID,
|
||||
// ORDER, TYPE_ALIASES, shortById(), …)
|
||||
// A future enum named 'BY_ID' or 'ORDER' is now impossible to confuse
|
||||
// with a helper key.
|
||||
//
|
||||
// BACK-COMPAT: every legacy access shape still works because we also
|
||||
// hang the enums + helpers at the root of the same object — direct
|
||||
// `PayloadLabels.GRP_TXT.short` and `PayloadLabels.SHORT_BY_ID`
|
||||
// continue to resolve. Removed in a future cycle.
|
||||
var api = {
|
||||
LABELS: PAYLOAD_LABELS,
|
||||
ORDER: ORDER,
|
||||
BY_ID: BY_ID,
|
||||
FW_PAYLOAD_TYPES: FW_PAYLOAD_TYPES,
|
||||
SHORT_BY_ID: SHORT_BY_ID,
|
||||
TYPE_ALIASES: TYPE_ALIASES,
|
||||
shortById: function (id) { return SHORT_BY_ID[id]; },
|
||||
longById: function (id) { return (BY_ID[id] && BY_ID[id].long) || ''; },
|
||||
enumNameById: function (id) { return FW_PAYLOAD_TYPES[id]; }
|
||||
};
|
||||
var ns = {
|
||||
enums: PAYLOAD_LABELS,
|
||||
api: api,
|
||||
// Legacy root-level helpers — kept for callers that haven't migrated
|
||||
// to .api yet. Same object references, so a new enum can't shadow
|
||||
// them at runtime (collisions still possible structurally, but the
|
||||
// .enums/.api split is the safe path).
|
||||
LABELS: PAYLOAD_LABELS,
|
||||
ORDER: ORDER,
|
||||
BY_ID: BY_ID,
|
||||
FW_PAYLOAD_TYPES: FW_PAYLOAD_TYPES,
|
||||
SHORT_BY_ID: SHORT_BY_ID,
|
||||
TYPE_ALIASES: TYPE_ALIASES,
|
||||
shortById: api.shortById,
|
||||
longById: api.longById,
|
||||
enumNameById: api.enumNameById
|
||||
};
|
||||
// Mirror each enum at the root for legacy direct-prop access
|
||||
// (`window.PayloadLabels.GRP_TXT.short`). New code should read
|
||||
// `.enums[GRP_TXT].short` and let this mirror retire.
|
||||
Object.entries(PAYLOAD_LABELS).forEach(function (kv) {
|
||||
ns[kv[0]] = kv[1];
|
||||
});
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.PayloadLabels = ns;
|
||||
}
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = ns;
|
||||
}
|
||||
})();
|
||||
+10
-3
@@ -163,9 +163,16 @@
|
||||
value: _roleOverrides, writable: false, enumerable: false, configurable: false
|
||||
});
|
||||
|
||||
// PR #1804 r1 item 4 (tufte4+adv5): ACK and UNKNOWN deliberately share
|
||||
// the neutral #6b7280 swatch (both render as the "Other" bucket). The
|
||||
// legend now identifies rows via data-enum="<ENUM>", so the reverse
|
||||
// color→enum lookup is no longer needed for tests or for the runtime
|
||||
// — the prior insertion-order workaround is gone, restoring natural
|
||||
// declaration order.
|
||||
window.TYPE_COLORS = {
|
||||
ADVERT: '#22c55e', GRP_TXT: '#3b82f6', GRP_DATA: '#8b5cf6', TXT_MSG: '#f59e0b', ACK: '#6b7280',
|
||||
REQUEST: '#a855f7', RESPONSE: '#06b6d4', TRACE: '#ec4899', PATH: '#14b8a6',
|
||||
ADVERT: '#22c55e', GRP_TXT: '#3b82f6', GRP_DATA: '#8b5cf6', TXT_MSG: '#f59e0b',
|
||||
ACK: '#6b7280',
|
||||
REQ: '#a855f7', RESPONSE: '#06b6d4', TRACE: '#ec4899', PATH: '#14b8a6',
|
||||
ANON_REQ: '#f43f5e', MULTIPART: '#0d9488', CONTROL: '#b45309', RAW_CUSTOM: '#c026d3',
|
||||
UNKNOWN: '#6b7280'
|
||||
};
|
||||
@@ -173,7 +180,7 @@
|
||||
// Badge CSS class name mapping
|
||||
const TYPE_BADGE_MAP = {
|
||||
ADVERT: 'advert', GRP_TXT: 'grp-txt', GRP_DATA: 'grp-data', TXT_MSG: 'txt-msg', ACK: 'ack',
|
||||
REQUEST: 'req', RESPONSE: 'response', TRACE: 'trace', PATH: 'path',
|
||||
REQ: 'req', RESPONSE: 'response', TRACE: 'trace', PATH: 'path',
|
||||
ANON_REQ: 'anon-req', MULTIPART: 'multipart', CONTROL: 'control', RAW_CUSTOM: 'raw-custom',
|
||||
UNKNOWN: 'unknown'
|
||||
};
|
||||
|
||||
@@ -69,7 +69,9 @@
|
||||
case 'RESPONSE':
|
||||
case 'ANON_REQ':
|
||||
var typeGlyphNames = { 'TXT_MSG': 'ph-envelope', 'REQ': 'ph-lock', 'RESPONSE': 'ph-lock-open', 'ANON_REQ': 'ph-lock' };
|
||||
var typeLabels = { 'TXT_MSG': 'DM', 'REQ': 'REQUEST', 'RESPONSE': 'RESPONSE', 'ANON_REQ': 'ANON REQ' };
|
||||
// PR #1804 r1 item 10: REQ label uses the canonical short
|
||||
// ('Request') instead of the legacy 'REQUEST' caps string.
|
||||
var typeLabels = { 'TXT_MSG': 'DM', 'REQ': 'Request', 'RESPONSE': 'Response', 'ANON_REQ': 'Anon Req' };
|
||||
glyph = (typeGlyphNames[t] ? '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#' + typeGlyphNames[t] + '"/></svg>' : '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-circle"/></svg>');
|
||||
label = typeLabels[t] || t;
|
||||
var src = pktCtx.srcResolvedName || (d.srcHash ? 'unknown (hash ' + d.srcHash + ')' : (t === 'ANON_REQ' ? 'anon' : '?'));
|
||||
|
||||
@@ -63,8 +63,10 @@ function makeSandbox() {
|
||||
|
||||
function loadCustomizer() {
|
||||
const ctx = makeSandbox();
|
||||
const labelsCode = fs.readFileSync('public/payload-labels.js', 'utf8');
|
||||
const code = fs.readFileSync('public/customize-v2.js', 'utf8');
|
||||
vm.createContext(ctx);
|
||||
vm.runInContext(labelsCode, ctx, { filename: 'payload-labels.js' });
|
||||
vm.runInContext(code, ctx, { filename: 'customize-v2.js' });
|
||||
return { ctx, api: ctx.window._customizerV2, ls: ctx.localStorage };
|
||||
}
|
||||
|
||||
@@ -89,6 +89,13 @@ function makeSandbox() {
|
||||
}
|
||||
|
||||
function loadInCtx(ctx, file) {
|
||||
// PR #1804 r1 item 9: live.js / packets.js / customize-v2.js / packet-filter.js
|
||||
// now hard-fail if window.PayloadLabels is missing. Auto-preload payload-labels.js
|
||||
// (idempotent) before any drift-gated file so tests exercise the prod path.
|
||||
if (!ctx.__payloadLabelsLoaded && file !== 'public/payload-labels.js') {
|
||||
ctx.__payloadLabelsLoaded = true;
|
||||
vm.runInContext(fs.readFileSync('public/payload-labels.js', 'utf8'), ctx);
|
||||
}
|
||||
vm.runInContext(fs.readFileSync(file, 'utf8'), ctx);
|
||||
// Copy window.* to global context so bare references work
|
||||
for (const k of Object.keys(ctx.window)) {
|
||||
|
||||
@@ -84,6 +84,7 @@ function load(ctx, file) {
|
||||
|
||||
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');
|
||||
|
||||
|
||||
@@ -3,9 +3,14 @@
|
||||
const vm = require('vm');
|
||||
const fs = require('fs');
|
||||
|
||||
const labelsCode = fs.readFileSync('public/payload-labels.js', 'utf8');
|
||||
const code = fs.readFileSync('public/packet-filter.js', 'utf8');
|
||||
const ctx = { window: {}, console };
|
||||
vm.createContext(ctx);
|
||||
// PR #1804 r1 item 9: packet-filter.js hard-fails if window.PayloadLabels
|
||||
// is missing. Pre-load the canonical module so the test exercises the
|
||||
// production code path.
|
||||
vm.runInContext(labelsCode, ctx);
|
||||
vm.runInContext(code, ctx);
|
||||
const PF = ctx.window.PacketFilter;
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ function assert(cond, msg) {
|
||||
|
||||
const indexSrc = fs.readFileSync(path.join(__dirname, 'public', 'index.html'), 'utf8');
|
||||
const customSrc = fs.readFileSync(path.join(__dirname, 'public', 'customize-v2.js'), 'utf8');
|
||||
const labelsSrc = fs.readFileSync(path.join(__dirname, 'public', 'payload-labels.js'), 'utf8');
|
||||
|
||||
console.log('\n=== #1380 A: index.html has inline SVG filters for the 4 sim classes ===');
|
||||
['cb-deut', 'cb-prot', 'cb-trit', 'cb-achromat'].forEach(function (id) {
|
||||
@@ -137,6 +138,7 @@ let envOK = false, env, exposed;
|
||||
try {
|
||||
env = makeSandbox();
|
||||
vm.createContext(env.sandbox);
|
||||
vm.runInContext(labelsSrc, env.sandbox, { filename: 'payload-labels.js' });
|
||||
vm.runInContext(customSrc, env.sandbox, { filename: 'customize-v2.js' });
|
||||
exposed = env.sandbox.window._customizerV2;
|
||||
envOK = !!exposed;
|
||||
|
||||
@@ -87,8 +87,10 @@ function makeSandbox(opts) {
|
||||
|
||||
function loadCustomizer(opts) {
|
||||
const { ctx, cssProps } = makeSandbox(opts);
|
||||
const labelsCode = fs.readFileSync('public/payload-labels.js', 'utf8');
|
||||
const code = fs.readFileSync('public/customize-v2.js', 'utf8');
|
||||
vm.createContext(ctx);
|
||||
vm.runInContext(labelsCode, ctx, { filename: 'payload-labels.js' });
|
||||
vm.runInContext(code, ctx, { filename: 'customize-v2.js' });
|
||||
return { api: ctx.window._customizerV2, cssProps, ls: ctx.localStorage };
|
||||
}
|
||||
|
||||
@@ -83,8 +83,10 @@ function makeSandbox(opts) {
|
||||
|
||||
function loadCustomizer(opts) {
|
||||
const { ctx, cssProps } = makeSandbox(opts);
|
||||
const labelsCode = fs.readFileSync('public/payload-labels.js', 'utf8');
|
||||
const code = fs.readFileSync('public/customize-v2.js', 'utf8');
|
||||
vm.createContext(ctx);
|
||||
vm.runInContext(labelsCode, ctx, { filename: 'payload-labels.js' });
|
||||
vm.runInContext(code, ctx, { filename: 'customize-v2.js' });
|
||||
return { api: ctx.window._customizerV2, cssProps, ls: ctx.localStorage };
|
||||
}
|
||||
|
||||
@@ -73,8 +73,10 @@ function makeSandbox() {
|
||||
|
||||
function load() {
|
||||
const ctx = makeSandbox();
|
||||
const labelsCode = fs.readFileSync('public/payload-labels.js', 'utf8');
|
||||
const code = fs.readFileSync('public/customize-v2.js', 'utf8');
|
||||
vm.createContext(ctx);
|
||||
vm.runInContext(labelsCode, ctx, { filename: 'payload-labels.js' });
|
||||
vm.runInContext(code, ctx, { filename: 'customize-v2.js' });
|
||||
return { ctx, api: ctx.window._customizerV2 };
|
||||
}
|
||||
|
||||
@@ -146,6 +146,7 @@ function makeLiveSandbox() {
|
||||
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];
|
||||
|
||||
@@ -275,7 +275,7 @@ function parseRgbFromCss(token) {
|
||||
|
||||
const TYPE_BADGE_MAP_LOCAL = {
|
||||
ADVERT: 'advert', GRP_TXT: 'grp-txt', GRP_DATA: 'grp-data', TXT_MSG: 'txt-msg', ACK: 'ack',
|
||||
REQUEST: 'req', RESPONSE: 'response', TRACE: 'trace', PATH: 'path',
|
||||
REQ: 'req', RESPONSE: 'response', TRACE: 'trace', PATH: 'path',
|
||||
ANON_REQ: 'anon-req', MULTIPART: 'multipart', CONTROL: 'control', RAW_CUSTOM: 'raw-custom',
|
||||
UNKNOWN: 'unknown',
|
||||
};
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
/**
|
||||
* E2E for #1799 — canonical payload label vocabulary across surfaces.
|
||||
*
|
||||
* After PR #1804 round-1 review:
|
||||
* - Item 12: literal pinned expected labels (not derived from the same
|
||||
* map being tested) + inline-fallback drift gate.
|
||||
* - Item 13: explicit TYPE_ALIASES coverage through PacketFilter.
|
||||
* - Item 14: every key in the canonical map is exercised against pinned
|
||||
* literals — not just the original 3 enums.
|
||||
*
|
||||
* Run: BASE_URL=http://localhost:13581 node test-issue-1799-label-vocab-e2e.js
|
||||
*/
|
||||
'use strict';
|
||||
const { chromium } = require('playwright');
|
||||
|
||||
const BASE = process.env.BASE_URL || 'http://localhost:13581';
|
||||
|
||||
let passed = 0, failed = 0;
|
||||
async function step(name, fn) {
|
||||
try { await fn(); passed++; console.log(' \u2713 ' + name); }
|
||||
catch (e) { failed++; console.error(' \u2717 ' + name + ': ' + e.message); }
|
||||
}
|
||||
function assert(c, m) { if (!c) throw new Error(m || 'assertion failed'); }
|
||||
|
||||
// PINNED expected literals — these MUST match public/payload-labels.js by
|
||||
// hand. The whole point of pinning (round-1 review item 12) is that if
|
||||
// either side drifts, the test fails — NOT because both sides derive from
|
||||
// the same object.
|
||||
const EXPECTED_SHORT = {
|
||||
REQ: 'Request',
|
||||
RESPONSE: 'Response',
|
||||
TXT_MSG: 'Direct Msg',
|
||||
ACK: 'ACK',
|
||||
ADVERT: 'Advert',
|
||||
GRP_TXT: 'Channel Msg',
|
||||
GRP_DATA: 'Group Data',
|
||||
ANON_REQ: 'Anon Req',
|
||||
PATH: 'Path',
|
||||
TRACE: 'Trace',
|
||||
MULTIPART: 'Multipart',
|
||||
CONTROL: 'Control',
|
||||
RAW_CUSTOM: 'Raw Custom'
|
||||
};
|
||||
// PR #1804 r1 item 2 (tufte2): every `long` must DESCRIBE the packet's
|
||||
// behaviour, not echo the short label. Pinned literals — drift here
|
||||
// fails the E2E.
|
||||
const EXPECTED_LONG = {
|
||||
REQ: 'Encrypted data request to a remote node',
|
||||
RESPONSE: 'Encrypted data response from a remote node',
|
||||
TXT_MSG: 'Encrypted point-to-point text message',
|
||||
ACK: 'Acknowledgment of a prior message or request',
|
||||
ADVERT: 'Node identity and capability advertisement',
|
||||
GRP_TXT: 'Channel-scoped group text message',
|
||||
GRP_DATA: 'Channel-scoped group datagram (non-text payload)',
|
||||
ANON_REQ: 'Anonymous encrypted request via ephemeral key',
|
||||
PATH: 'Network path discovery and return-path advertisement',
|
||||
TRACE: 'Per-hop route trace with SNR samples',
|
||||
MULTIPART: 'Fragmented payload reassembled across multiple packets',
|
||||
CONTROL: 'Mesh control-plane signalling (e.g. zero-hop direct)',
|
||||
RAW_CUSTOM: 'Application-defined raw payload, no firmware envelope'
|
||||
};
|
||||
const EXPECTED_ID = {
|
||||
REQ: 0, RESPONSE: 1, TXT_MSG: 2, ACK: 3, ADVERT: 4, GRP_TXT: 5,
|
||||
GRP_DATA: 6, ANON_REQ: 7, PATH: 8, TRACE: 9, MULTIPART: 10,
|
||||
CONTROL: 11, RAW_CUSTOM: 15
|
||||
};
|
||||
const ALL_ENUMS = Object.keys(EXPECTED_SHORT);
|
||||
|
||||
async function gotoLive(page) {
|
||||
await page.goto(BASE + '/#/live', { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForSelector('#liveLegend', { timeout: 10000, state: 'attached' });
|
||||
await page.evaluate(() => {
|
||||
try { localStorage.removeItem('live-legend-hidden'); } catch (_) {}
|
||||
const el = document.getElementById('liveLegend');
|
||||
if (el) el.classList.remove('hidden');
|
||||
});
|
||||
await page.waitForTimeout(300);
|
||||
}
|
||||
|
||||
// Pull "short" label from each legend row. Build the color→enum reverse
|
||||
// map from TYPE_COLORS so we can identify rows by enum without trusting
|
||||
// the rendered text.
|
||||
async function legendShortLabels(page) {
|
||||
return page.evaluate(() => {
|
||||
const out = {};
|
||||
const el = document.getElementById('liveLegend');
|
||||
if (!el) return out;
|
||||
const lis = el.querySelectorAll('.legend-list li');
|
||||
const TYPE_COLORS = window.TYPE_COLORS || {};
|
||||
const colorToEnum = {};
|
||||
for (const k of Object.keys(TYPE_COLORS)) colorToEnum[String(TYPE_COLORS[k]).toLowerCase()] = k;
|
||||
for (const li of lis) {
|
||||
// PR #1804 r1 item 4 (tufte4+adv5): rows now carry data-enum, so we
|
||||
// identify by enum directly instead of reverse-mapping via the
|
||||
// shared #6b7280 color (which forced an insertion-order workaround
|
||||
// in roles.js). Fall back to the color path only if data-enum is
|
||||
// missing, for robustness while the change rolls out.
|
||||
const enumAttr = li.getAttribute('data-enum');
|
||||
const dot = li.querySelector('.live-dot');
|
||||
let enumName = enumAttr || '';
|
||||
if (!enumName) {
|
||||
if (!dot) continue;
|
||||
const styleAttr = dot.getAttribute('style') || '';
|
||||
const mhex = styleAttr.match(/#([0-9a-f]{3,8})/i);
|
||||
const color = mhex ? ('#' + mhex[1].toLowerCase()) : '';
|
||||
enumName = colorToEnum[color];
|
||||
if (!enumName) continue;
|
||||
}
|
||||
const txt = (li.textContent || '').trim();
|
||||
// PR #1804 r1 item 1 (tufte1+adv1): all rows render with the same
|
||||
// em-dash separator now (no slash special-case for ACK), so a
|
||||
// single split rule applies.
|
||||
const parts = txt.split(/\s+\u2014\s+/);
|
||||
out[enumName] = parts[0].trim();
|
||||
}
|
||||
return out;
|
||||
});
|
||||
}
|
||||
|
||||
async function gotoPackets(page) {
|
||||
await page.evaluate(() => {
|
||||
try {
|
||||
localStorage.removeItem('meshcore-groupbyhash');
|
||||
localStorage.setItem('meshcore-time-window', '525600');
|
||||
} catch (_) {}
|
||||
});
|
||||
await page.goto(BASE + '/#/packets', { waitUntil: 'domcontentloaded' });
|
||||
await page.reload({ waitUntil: 'load' });
|
||||
await page.waitForSelector('#typeTrigger', { timeout: 15000 });
|
||||
await page.click('#typeTrigger');
|
||||
await page.waitForSelector('#typeMenu .multi-select-item', { timeout: 5000 });
|
||||
}
|
||||
|
||||
async function packetsTypeLabels(page) {
|
||||
return page.evaluate(() => {
|
||||
const out = {};
|
||||
const items = document.querySelectorAll('#typeMenu .multi-select-item');
|
||||
for (const lab of items) {
|
||||
const cb = lab.querySelector('input[type=checkbox]');
|
||||
if (!cb) continue;
|
||||
const id = cb.getAttribute('data-type-id');
|
||||
if (id === '__all__') continue;
|
||||
out[id] = (lab.textContent || '').trim();
|
||||
}
|
||||
return out;
|
||||
});
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.launch({
|
||||
headless: true,
|
||||
executablePath: process.env.CHROMIUM_PATH || undefined,
|
||||
args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'],
|
||||
});
|
||||
|
||||
console.log(`\n=== #1799 canonical payload label vocabulary — E2E against ${BASE} ===`);
|
||||
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
page.setDefaultTimeout(10000);
|
||||
page.on('pageerror', (e) => console.error('[pageerror]', e.message));
|
||||
|
||||
await step('navigate to /live and read legend short labels', async () => { await gotoLive(page); });
|
||||
const legend = await legendShortLabels(page);
|
||||
|
||||
await step('canonical map exposed as window.PayloadLabels on /live', async () => {
|
||||
const pl = await page.evaluate(() => window.PayloadLabels || null);
|
||||
assert(pl && typeof pl === 'object', 'window.PayloadLabels missing');
|
||||
// Pinned-literal check, NOT a self-derived comparison (round-1 item 12).
|
||||
for (const name of ALL_ENUMS) {
|
||||
assert(pl[name], `PayloadLabels.${name} missing`);
|
||||
assert(pl[name].short === EXPECTED_SHORT[name],
|
||||
`PayloadLabels.${name}.short: expected "${EXPECTED_SHORT[name]}", got "${pl[name].short}"`);
|
||||
assert(pl[name].enumId === EXPECTED_ID[name],
|
||||
`PayloadLabels.${name}.enumId: expected ${EXPECTED_ID[name]}, got ${pl[name].enumId}`);
|
||||
assert(pl[name].enumName === name,
|
||||
`PayloadLabels.${name}.enumName: expected "${name}", got "${pl[name].enumName}"`);
|
||||
// PR #1804 r1 item 2: long must be the behavioural description, not
|
||||
// a tautological echo of short. Pinned literals.
|
||||
assert(pl[name].long === EXPECTED_LONG[name],
|
||||
`PayloadLabels.${name}.long: expected "${EXPECTED_LONG[name]}", got "${pl[name].long}"`);
|
||||
}
|
||||
});
|
||||
|
||||
await step('every legend row matches the pinned canonical short label', async () => {
|
||||
// Round-1 item 14: cover ALL 13 enums, not just 3.
|
||||
for (const name of ALL_ENUMS) {
|
||||
const got = legend[name];
|
||||
assert(got === EXPECTED_SHORT[name],
|
||||
`legend[${name}]: expected "${EXPECTED_SHORT[name]}", got "${got}" (full legend: ${JSON.stringify(legend)})`);
|
||||
}
|
||||
});
|
||||
|
||||
await step('every legend row carries data-enum=<ENUM_NAME> (PR #1804 r1 item 4)', async () => {
|
||||
// tufte4+adv5: rows must be identifiable by enum, not by reverse-
|
||||
// mapping the shared #6b7280 color.
|
||||
const rows = await page.evaluate(() => {
|
||||
const el = document.getElementById('liveLegend');
|
||||
if (!el) return [];
|
||||
return Array.from(el.querySelectorAll('.legend-list li')).map(li => ({
|
||||
en: li.getAttribute('data-enum'),
|
||||
text: (li.textContent || '').trim()
|
||||
}));
|
||||
});
|
||||
const enumsSeen = new Set();
|
||||
for (const r of rows) {
|
||||
// Only legend rows with a live-dot are payload-type rows. Other
|
||||
// <li>s (e.g. role legend, ring legend) may not carry data-enum.
|
||||
if (!r.en) continue;
|
||||
enumsSeen.add(r.en);
|
||||
}
|
||||
for (const name of ALL_ENUMS) {
|
||||
assert(enumsSeen.has(name),
|
||||
`data-enum="${name}" missing on legend row (rows=${JSON.stringify(rows)})`);
|
||||
}
|
||||
});
|
||||
|
||||
await step('all legend rows render with the uniform em-dash separator (PR #1804 r1 item 1)', async () => {
|
||||
// tufte1+adv1: ACK row used to render with a slash + 'Other —'
|
||||
// wrapper. Now every row is `SHORT — LONG`.
|
||||
const rows = await page.evaluate(() => {
|
||||
const el = document.getElementById('liveLegend');
|
||||
if (!el) return [];
|
||||
return Array.from(el.querySelectorAll('.legend-list li[data-enum]'))
|
||||
.map(li => (li.textContent || '').trim());
|
||||
});
|
||||
for (const t of rows) {
|
||||
assert(t.indexOf('\u2014') !== -1, `legend row missing em-dash: "${t}"`);
|
||||
assert(t.indexOf(' / ') === -1, `legend row uses slash separator: "${t}"`);
|
||||
}
|
||||
});
|
||||
|
||||
await step('canonical map exposed at window.PayloadLabels.enums (PR #1804 r1 item 8)', async () => {
|
||||
const ok = await page.evaluate((enums) => {
|
||||
const PL = window.PayloadLabels;
|
||||
if (!PL || !PL.enums || !PL.api) return { ok: false, why: 'missing PL/enums/api' };
|
||||
for (const k of enums) {
|
||||
if (!PL.enums[k]) return { ok: false, why: 'PL.enums.' + k + ' missing' };
|
||||
if (!PL.api.SHORT_BY_ID) return { ok: false, why: 'PL.api.SHORT_BY_ID missing' };
|
||||
}
|
||||
return { ok: true };
|
||||
}, ALL_ENUMS);
|
||||
assert(ok.ok, 'namespace check: ' + (ok.why || 'unknown'));
|
||||
});
|
||||
|
||||
await step('navigate to /packets and open type filter', async () => { await gotoPackets(page); });
|
||||
const packetsLabels = await packetsTypeLabels(page);
|
||||
|
||||
await step('canonical map also exposed on /packets', async () => {
|
||||
const pl = await page.evaluate(() => window.PayloadLabels || null);
|
||||
assert(pl && typeof pl === 'object', 'window.PayloadLabels missing on /packets');
|
||||
});
|
||||
|
||||
await step('every packets type-filter row matches the pinned canonical short label', async () => {
|
||||
// Round-1 item 14: cover ALL enums on the packets page too.
|
||||
for (const name of ALL_ENUMS) {
|
||||
const id = String(EXPECTED_ID[name]);
|
||||
const got = packetsLabels[id];
|
||||
assert(got === EXPECTED_SHORT[name],
|
||||
`packets type-menu[id=${id} (${name})]: expected "${EXPECTED_SHORT[name]}", got "${got}"`);
|
||||
}
|
||||
});
|
||||
|
||||
await step('PacketFilter recognises every enum name (round-trip)', async () => {
|
||||
const pf = await page.evaluate((enums) => {
|
||||
const pf = window.PacketFilter; if (!pf) return null;
|
||||
const out = {};
|
||||
for (const e of enums) {
|
||||
const c = pf.compile('type == ' + e.name);
|
||||
out[e.name] = !c.error && c.filter({ payload_type: e.id }) === true;
|
||||
}
|
||||
return out;
|
||||
}, ALL_ENUMS.map(n => ({ name: n, id: EXPECTED_ID[n] })));
|
||||
assert(pf, 'window.PacketFilter missing');
|
||||
for (const name of ALL_ENUMS) {
|
||||
assert(pf[name], `packet-filter does not recognise enum name "${name}"`);
|
||||
}
|
||||
});
|
||||
|
||||
await step('PacketFilter resolves TYPE_ALIASES through PacketFilter.compile (round-1 item 13)', async () => {
|
||||
// Map of alias → expected enumId. Each must resolve via the filter
|
||||
// language (quoted alias values use the same alias table). Covers the
|
||||
// path that round-1 item 13 flagged as untested.
|
||||
const ALIAS_CASES = [
|
||||
{ alias: 'channel msg', id: 5 },
|
||||
{ alias: 'dm', id: 2 },
|
||||
{ alias: 'direct msg', id: 2 },
|
||||
{ alias: 'group data', id: 6 },
|
||||
{ alias: 'raw custom', id: 15 },
|
||||
{ alias: 'anon req', id: 7 },
|
||||
{ alias: 'request', id: 0 }
|
||||
];
|
||||
const got = await page.evaluate((cases) => {
|
||||
const pf = window.PacketFilter; if (!pf) return null;
|
||||
return cases.map(c => {
|
||||
const compiled = pf.compile('type == "' + c.alias + '"');
|
||||
return {
|
||||
alias: c.alias,
|
||||
id: c.id,
|
||||
ok: !compiled.error && compiled.filter({ payload_type: c.id }) === true,
|
||||
err: compiled.error || null
|
||||
};
|
||||
});
|
||||
}, ALIAS_CASES);
|
||||
assert(got, 'window.PacketFilter missing');
|
||||
for (const r of got) {
|
||||
assert(r.ok, `alias "${r.alias}" → payload_type=${r.id} failed (err=${r.err})`);
|
||||
}
|
||||
});
|
||||
|
||||
await ctx.close();
|
||||
await browser.close();
|
||||
console.log(`\n=== ${passed} passed, ${failed} failed ===`);
|
||||
process.exit(failed === 0 ? 0 : 1);
|
||||
})().catch((e) => { console.error(e); process.exit(1); });
|
||||
@@ -61,6 +61,7 @@ const ctx = {
|
||||
};
|
||||
vm.createContext(ctx);
|
||||
|
||||
vm.runInContext(fs.readFileSync('public/payload-labels.js', 'utf8'), ctx);
|
||||
const src = fs.readFileSync('public/live.js', 'utf8');
|
||||
vm.runInContext(src, ctx);
|
||||
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
/* Unit test for buildLegendHtml() — the helper extracted from live.js
|
||||
* legend rendering IIFE per PR #1804 r1 item 7 (adv3).
|
||||
*
|
||||
* Loads the helper via vm with a stubbed window so the function is
|
||||
* directly testable without spinning Playwright + chromium.
|
||||
*/
|
||||
'use strict';
|
||||
const fs = require('fs');
|
||||
const vm = require('vm');
|
||||
|
||||
const labelsSrc = fs.readFileSync('public/payload-labels.js', 'utf8');
|
||||
const liveSrc = fs.readFileSync('public/live.js', 'utf8');
|
||||
|
||||
const ctx = {
|
||||
window: {},
|
||||
console,
|
||||
document: { addEventListener: () => {}, getElementById: () => null, querySelector: () => null, querySelectorAll: () => [] },
|
||||
navigator: { userAgent: 'node' },
|
||||
location: { hash: '', pathname: '/' },
|
||||
localStorage: { getItem: () => null, setItem: () => {}, removeItem: () => {} },
|
||||
setTimeout, clearTimeout, setInterval, clearInterval,
|
||||
fetch: () => Promise.resolve({ ok: true, json: () => ({}) }),
|
||||
};
|
||||
ctx.self = ctx; ctx.globalThis = ctx;
|
||||
vm.createContext(ctx);
|
||||
|
||||
// Load canonical labels first.
|
||||
vm.runInContext(labelsSrc, ctx);
|
||||
|
||||
// live.js is a huge file with many module-scope side effects (router,
|
||||
// websocket, etc). We only need buildLegendHtml. Extract it by locating
|
||||
// `function buildLegendHtml(...)` and brace-counting to the matching '}'.
|
||||
function extractHelper(src) {
|
||||
const startMarker = 'function buildLegendHtml';
|
||||
const idx = src.indexOf(startMarker);
|
||||
if (idx === -1) return null;
|
||||
// find the '{' opening the body
|
||||
let i = src.indexOf('{', idx);
|
||||
if (i === -1) return null;
|
||||
let depth = 0;
|
||||
for (; i < src.length; i++) {
|
||||
if (src[i] === '{') depth++;
|
||||
else if (src[i] === '}') { depth--; if (depth === 0) { return src.slice(idx, i + 1); } }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const helperSrc = extractHelper(liveSrc);
|
||||
if (!helperSrc) {
|
||||
// Helper hasn't been extracted yet — that's the RED state.
|
||||
console.log(' ✗ buildLegendHtml helper not found in public/live.js — extract it (PR #1804 r1 item 7)');
|
||||
console.log('=== 0 passed, 1 failed ===');
|
||||
process.exit(1);
|
||||
}
|
||||
vm.runInContext('var TYPE_COLORS = { ADVERT: "#22c55e", GRP_TXT: "#3b82f6", GRP_DATA: "#8b5cf6", TXT_MSG: "#f59e0b", ACK: "#6b7280", REQ: "#a855f7", RESPONSE: "#06b6d4", TRACE: "#ec4899", PATH: "#14b8a6", ANON_REQ: "#f43f5e", MULTIPART: "#0d9488", CONTROL: "#b45309", RAW_CUSTOM: "#c026d3" };', ctx);
|
||||
vm.runInContext(helperSrc, ctx);
|
||||
const buildLegendHtml = ctx.buildLegendHtml;
|
||||
|
||||
let pass = 0, fail = 0;
|
||||
function test(name, fn) {
|
||||
try { fn(); pass++; console.log(' \u2713 ' + name); }
|
||||
catch (e) { fail++; console.log(' \u2717 ' + name + ' — ' + e.message); }
|
||||
}
|
||||
function assert(c, m) { if (!c) throw new Error(m || 'assertion failed'); }
|
||||
|
||||
const html = buildLegendHtml(ctx.window.PayloadLabels);
|
||||
|
||||
test('returns a non-empty string', () => {
|
||||
assert(typeof html === 'string' && html.length > 0, 'got ' + typeof html);
|
||||
});
|
||||
|
||||
test('emits one <li> per enum in PayloadLabels.ORDER', () => {
|
||||
const liCount = (html.match(/<li\b/g) || []).length;
|
||||
const want = ctx.window.PayloadLabels.ORDER.length;
|
||||
assert(liCount === want, 'li count: want ' + want + ', got ' + liCount);
|
||||
});
|
||||
|
||||
test('every row carries data-enum="<ENUM>" attribute', () => {
|
||||
for (const k of ctx.window.PayloadLabels.ORDER) {
|
||||
assert(html.indexOf('data-enum="' + k + '"') !== -1,
|
||||
'data-enum="' + k + '" missing');
|
||||
}
|
||||
});
|
||||
|
||||
test('every row uses the em-dash separator (uniform typography, item 1)', () => {
|
||||
for (const k of ctx.window.PayloadLabels.ORDER) {
|
||||
const entry = ctx.window.PayloadLabels[k];
|
||||
const wantSnippet = entry.short + ' \u2014 ' + entry.long;
|
||||
assert(html.indexOf(wantSnippet) !== -1,
|
||||
k + ': expected snippet "' + wantSnippet + '" not found in legend html');
|
||||
}
|
||||
});
|
||||
|
||||
test('no row uses " / " as the SHORT/LONG separator (uniform typography)', () => {
|
||||
// Slashes are fine inside long descriptions (e.g. "path discovery /
|
||||
// return-path advertisement"). Forbid only the legacy `SHORT / Other —`
|
||||
// structure that used to render ACK differently.
|
||||
for (const k of ctx.window.PayloadLabels.ORDER) {
|
||||
const entry = ctx.window.PayloadLabels[k];
|
||||
const badSnippet = entry.short + ' / ';
|
||||
assert(html.indexOf(badSnippet) === -1,
|
||||
k + ': legacy slash-after-short separator survived ("' + badSnippet + '")');
|
||||
}
|
||||
});
|
||||
|
||||
console.log('=== ' + pass + ' passed, ' + fail + ' failed ===');
|
||||
process.exit(fail === 0 ? 0 : 1);
|
||||
@@ -83,6 +83,7 @@ function load(ctx, file) {
|
||||
|
||||
console.log('\n=== live.js: region filter (#1045) ===');
|
||||
const ctx = makeSandbox();
|
||||
load(ctx, 'public/payload-labels.js');
|
||||
load(ctx, 'public/roles.js');
|
||||
load(ctx, 'public/live.js');
|
||||
|
||||
|
||||
@@ -157,6 +157,7 @@ function makeLiveSandbox({ withAppJs = false } = {}) {
|
||||
const ctx = makeSandbox();
|
||||
addLiveGlobals(ctx);
|
||||
|
||||
loadInCtx(ctx, 'public/payload-labels.js');
|
||||
loadInCtx(ctx, 'public/roles.js');
|
||||
loadInCtx(ctx, 'public/packet-helpers.js');
|
||||
if (withAppJs) loadInCtx(ctx, 'public/app.js');
|
||||
@@ -288,6 +289,7 @@ console.log('\n=== live.js: expandToBufferEntriesAsync ===');
|
||||
// Build a sandbox with packet-helpers loaded so expandToBufferEntries can call dbPacketToLive
|
||||
const ctx = makeSandbox();
|
||||
addLiveGlobals(ctx);
|
||||
loadInCtx(ctx, 'public/payload-labels.js');
|
||||
loadInCtx(ctx, 'public/roles.js');
|
||||
loadInCtx(ctx, 'public/packet-helpers.js');
|
||||
try { loadInCtx(ctx, 'public/live.js'); } catch (e) {
|
||||
|
||||
@@ -3,9 +3,14 @@
|
||||
const vm = require('vm');
|
||||
const fs = require('fs');
|
||||
|
||||
const labelsCode = fs.readFileSync('public/payload-labels.js', 'utf8');
|
||||
const code = fs.readFileSync('public/packet-filter.js', 'utf8');
|
||||
const ctx = { window: {}, console };
|
||||
vm.createContext(ctx);
|
||||
// PR #1804 r1 item 9: packet-filter.js hard-fails if window.PayloadLabels
|
||||
// is missing. Pre-load the canonical module so the test exercises the
|
||||
// production code path.
|
||||
vm.runInContext(labelsCode, ctx);
|
||||
vm.runInContext(code, ctx);
|
||||
const PF = ctx.window.PacketFilter;
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ function test(name, fn) {
|
||||
function assert(c, m) { if (!c) throw new Error(m || 'assertion failed'); }
|
||||
|
||||
const ctx = makeCtx();
|
||||
loadInCtx(['public/packet-filter.js', 'public/filter-ux.js'], ctx);
|
||||
loadInCtx(['public/payload-labels.js', 'public/packet-filter.js', 'public/filter-ux.js'], ctx);
|
||||
const PF = ctx.window.PacketFilter;
|
||||
const UX = ctx.window.FilterUX;
|
||||
|
||||
|
||||
@@ -3,9 +3,14 @@
|
||||
const vm = require('vm');
|
||||
const fs = require('fs');
|
||||
|
||||
const labelsCode = fs.readFileSync('public/payload-labels.js', 'utf8');
|
||||
const code = fs.readFileSync('public/packet-filter.js', 'utf8');
|
||||
const ctx = { window: {}, console };
|
||||
vm.createContext(ctx);
|
||||
// PR #1804 r1 item 9: packet-filter.js now hard-fails if
|
||||
// window.PayloadLabels is missing. Pre-load the canonical module so the
|
||||
// test exercises the production code path.
|
||||
vm.runInContext(labelsCode, ctx);
|
||||
vm.runInContext(code, ctx);
|
||||
const PF = ctx.window.PacketFilter;
|
||||
|
||||
|
||||
@@ -105,6 +105,7 @@ function loadInCtx(ctx, file) {
|
||||
function loadPacketsSandbox() {
|
||||
const ctx = makeSandbox();
|
||||
// Load dependencies first
|
||||
loadInCtx(ctx, 'public/payload-labels.js');
|
||||
loadInCtx(ctx, 'public/roles.js');
|
||||
loadInCtx(ctx, 'public/app.js');
|
||||
loadInCtx(ctx, 'public/packet-helpers.js');
|
||||
@@ -1205,6 +1206,7 @@ console.log('\n=== packets.js: scroll position preserved across renderTableRows
|
||||
return null;
|
||||
};
|
||||
|
||||
loadInCtx(ctx, 'public/payload-labels.js');
|
||||
loadInCtx(ctx, 'public/roles.js');
|
||||
loadInCtx(ctx, 'public/app.js');
|
||||
loadInCtx(ctx, 'public/packet-helpers.js');
|
||||
|
||||
@@ -106,7 +106,7 @@ function loadLiveModule(ctx) {
|
||||
ctx.timeAgo = () => '—';
|
||||
ctx.getParsedPath = () => [];
|
||||
ctx.getParsedDecoded = () => ({});
|
||||
ctx.TYPE_COLORS = { ADVERT: '#22c55e', GRP_TXT: '#3b82f6', TXT_MSG: '#f59e0b', ACK: '#6b7280', REQUEST: '#a855f7', RESPONSE: '#06b6d4', TRACE: '#ec4899', PATH: '#14b8a6' };
|
||||
ctx.TYPE_COLORS = { ADVERT: '#22c55e', GRP_TXT: '#3b82f6', TXT_MSG: '#f59e0b', ACK: '#6b7280', REQ: '#a855f7', RESPONSE: '#06b6d4', TRACE: '#ec4899', PATH: '#14b8a6' };
|
||||
ctx.ROLE_COLORS = {};
|
||||
ctx.ROLE_LABELS = {};
|
||||
ctx.ROLE_STYLE = {};
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
/* Unit test for public/payload-labels.js — pins long descriptions and the
|
||||
* abbreviation-style policy declared at the top of the file.
|
||||
* #1799 PR #1804 r1 items 2 (tufte2: long must describe, not echo) and
|
||||
* 5 (tufte5: uniform abbreviation policy + top-of-file comment).
|
||||
*/
|
||||
'use strict';
|
||||
const fs = require('fs');
|
||||
const vm = require('vm');
|
||||
|
||||
const src = fs.readFileSync('public/payload-labels.js', 'utf8');
|
||||
const ctx = { window: {}, console };
|
||||
vm.createContext(ctx);
|
||||
vm.runInContext(src, ctx);
|
||||
const PL = ctx.window.PayloadLabels;
|
||||
|
||||
let pass = 0, fail = 0;
|
||||
function test(name, fn) {
|
||||
try { fn(); pass++; console.log(' \u2713 ' + name); }
|
||||
catch (e) { fail++; console.log(' \u2717 ' + name + ' — ' + e.message); }
|
||||
}
|
||||
function assert(c, m) { if (!c) throw new Error(m || 'assertion failed'); }
|
||||
|
||||
// Item 2 — long must describe behaviour, not echo the short label.
|
||||
const EXPECTED_LONG = {
|
||||
REQ: 'Encrypted data request to a remote node',
|
||||
RESPONSE: 'Encrypted data response from a remote node',
|
||||
TXT_MSG: 'Encrypted point-to-point text message',
|
||||
ACK: 'Acknowledgment of a prior message or request',
|
||||
ADVERT: 'Node identity and capability advertisement',
|
||||
GRP_TXT: 'Channel-scoped group text message',
|
||||
GRP_DATA: 'Channel-scoped group datagram (non-text payload)',
|
||||
ANON_REQ: 'Anonymous encrypted request via ephemeral key',
|
||||
PATH: 'Network path discovery and return-path advertisement',
|
||||
TRACE: 'Per-hop route trace with SNR samples',
|
||||
MULTIPART: 'Fragmented payload reassembled across multiple packets',
|
||||
CONTROL: 'Mesh control-plane signalling (e.g. zero-hop direct)',
|
||||
RAW_CUSTOM: 'Application-defined raw payload, no firmware envelope'
|
||||
};
|
||||
|
||||
console.log('=== payload-labels content + policy ===');
|
||||
|
||||
test('every enum has the pinned long description (item 2)', () => {
|
||||
for (const name of Object.keys(EXPECTED_LONG)) {
|
||||
assert(PL[name], 'missing entry ' + name);
|
||||
assert(PL[name].long === EXPECTED_LONG[name],
|
||||
name + '.long: expected "' + EXPECTED_LONG[name] + '", got "' + PL[name].long + '"');
|
||||
}
|
||||
});
|
||||
|
||||
test('no long tautologically echoes its short (item 2)', () => {
|
||||
for (const name of Object.keys(EXPECTED_LONG)) {
|
||||
const s = (PL[name].short || '').toLowerCase().replace(/\s+/g, ' ').trim();
|
||||
const l = (PL[name].long || '').toLowerCase().replace(/\s+/g, ' ').trim();
|
||||
assert(l !== s, name + ': long equals short (' + s + ')');
|
||||
assert(l !== s + ' ' + s, name + ': long is just doubled short');
|
||||
// soft heuristic: long must be at least 2 words longer than short
|
||||
const shortWords = s.split(/\s+/).length;
|
||||
const longWords = l.split(/\s+/).length;
|
||||
assert(longWords >= shortWords + 2,
|
||||
name + ': long ("' + l + '") not meaningfully longer than short ("' + s + '")');
|
||||
}
|
||||
});
|
||||
|
||||
test('abbreviation policy comment is present at top of file (item 5)', () => {
|
||||
// Top-of-file comment must declare the uniform policy so future editors
|
||||
// know the rule before adding a new enum.
|
||||
const head = src.slice(0, 1500);
|
||||
assert(/abbreviation policy|ABBREVIATION POLICY|Abbreviation policy/.test(head),
|
||||
'top-of-file comment missing an "abbreviation policy" section');
|
||||
});
|
||||
|
||||
test('every short respects the declared length cap (<=12 chars)', () => {
|
||||
for (const name of Object.keys(EXPECTED_LONG)) {
|
||||
assert(PL[name].short.length <= 12,
|
||||
name + '.short ("' + PL[name].short + '") exceeds 12-char cap');
|
||||
}
|
||||
});
|
||||
|
||||
test('all shorts use Title Case (no all-caps except documented exceptions)', () => {
|
||||
// ACK is the documented exception (a wire-protocol acronym).
|
||||
const allowedAllCaps = new Set(['ACK']);
|
||||
for (const name of Object.keys(EXPECTED_LONG)) {
|
||||
const s = PL[name].short;
|
||||
if (allowedAllCaps.has(name)) continue;
|
||||
assert(s !== s.toUpperCase() || s.length === 0,
|
||||
name + '.short ("' + s + '") is all-caps and not in the documented exception set');
|
||||
}
|
||||
});
|
||||
|
||||
console.log('=== ' + pass + ' passed, ' + fail + ' failed ===');
|
||||
process.exit(fail === 0 ? 0 : 1);
|
||||
@@ -0,0 +1,111 @@
|
||||
/* Unit test for the PayloadLabels namespace restructure (PR #1804 r1
|
||||
* items 8 & 9).
|
||||
*
|
||||
* Item 8: canonical enum map at window.PayloadLabels.enums[ENUM] and
|
||||
* helpers/derived at window.PayloadLabels.api — direct-prop
|
||||
* collisions (e.g. an enum named BY_ID or ORDER) become
|
||||
* impossible.
|
||||
* Item 9: payload-labels.js is loaded synchronously before its
|
||||
* consumers in index.html. Drop the inline fallbacks in
|
||||
* packets.js / packet-filter.js / live.js and add a top-of-file
|
||||
* guard so a missing module is a loud crash, not silent stale
|
||||
* data.
|
||||
*/
|
||||
'use strict';
|
||||
const fs = require('fs');
|
||||
const vm = require('vm');
|
||||
|
||||
const labelsSrc = fs.readFileSync('public/payload-labels.js', 'utf8');
|
||||
const packetsSrc = fs.readFileSync('public/packets.js', 'utf8');
|
||||
const filterSrc = fs.readFileSync('public/packet-filter.js', 'utf8');
|
||||
const liveSrc = fs.readFileSync('public/live.js', 'utf8');
|
||||
|
||||
const ctx = { window: {}, console };
|
||||
vm.createContext(ctx);
|
||||
vm.runInContext(labelsSrc, ctx);
|
||||
const PL = ctx.window.PayloadLabels;
|
||||
|
||||
let pass = 0, fail = 0;
|
||||
function test(name, fn) {
|
||||
try { fn(); pass++; console.log(' \u2713 ' + name); }
|
||||
catch (e) { fail++; console.log(' \u2717 ' + name + ' — ' + e.message); }
|
||||
}
|
||||
function assert(c, m) { if (!c) throw new Error(m || 'assertion failed'); }
|
||||
|
||||
const EXPECTED_ENUMS = [
|
||||
'REQ','RESPONSE','TXT_MSG','ACK','ADVERT','GRP_TXT','GRP_DATA',
|
||||
'ANON_REQ','PATH','TRACE','MULTIPART','CONTROL','RAW_CUSTOM'
|
||||
];
|
||||
|
||||
console.log('=== PayloadLabels namespace (item 8) ===');
|
||||
|
||||
test('window.PayloadLabels exposes .enums namespace', () => {
|
||||
assert(PL && typeof PL.enums === 'object', 'PayloadLabels.enums missing');
|
||||
for (const k of EXPECTED_ENUMS) {
|
||||
assert(PL.enums[k], 'PayloadLabels.enums.' + k + ' missing');
|
||||
assert(PL.enums[k].short, 'PayloadLabels.enums.' + k + '.short missing');
|
||||
assert(PL.enums[k].enumId !== undefined, 'PayloadLabels.enums.' + k + '.enumId missing');
|
||||
}
|
||||
});
|
||||
|
||||
test('window.PayloadLabels exposes .api namespace with helpers', () => {
|
||||
assert(PL.api && typeof PL.api === 'object', 'PayloadLabels.api missing');
|
||||
assert(typeof PL.api.shortById === 'function', 'PayloadLabels.api.shortById missing');
|
||||
assert(typeof PL.api.longById === 'function', 'PayloadLabels.api.longById missing');
|
||||
assert(typeof PL.api.enumNameById === 'function', 'PayloadLabels.api.enumNameById missing');
|
||||
assert(PL.api.SHORT_BY_ID, 'PayloadLabels.api.SHORT_BY_ID missing');
|
||||
assert(PL.api.FW_PAYLOAD_TYPES, 'PayloadLabels.api.FW_PAYLOAD_TYPES missing');
|
||||
assert(PL.api.TYPE_ALIASES, 'PayloadLabels.api.TYPE_ALIASES missing');
|
||||
assert(Array.isArray(PL.api.ORDER), 'PayloadLabels.api.ORDER missing');
|
||||
});
|
||||
|
||||
test('legacy direct-prop access still works (back-compat)', () => {
|
||||
// Existing callers (live.js, customize*.js, the E2E that pins literals)
|
||||
// read pl[ENUM].short directly. Keep that working — restructure adds
|
||||
// .enums/.api, doesn't break the direct shape.
|
||||
for (const k of EXPECTED_ENUMS) {
|
||||
assert(PL[k] && PL[k].short, 'legacy PayloadLabels.' + k + '.short broken');
|
||||
assert(PL[k] === PL.enums[k], 'legacy ' + k + ' should alias .enums.' + k);
|
||||
}
|
||||
// Helpers still reachable at root for back-compat too.
|
||||
assert(PL.SHORT_BY_ID, 'legacy PayloadLabels.SHORT_BY_ID missing');
|
||||
assert(PL.ORDER, 'legacy PayloadLabels.ORDER missing');
|
||||
});
|
||||
|
||||
console.log('=== inline-fallback maps (item 9) ===');
|
||||
|
||||
test('packets.js: DEFAULT_TYPE_NAMES inline fallback is gone', () => {
|
||||
assert(!/DEFAULT_TYPE_NAMES\s*=\s*\{/.test(packetsSrc),
|
||||
'packets.js still declares DEFAULT_TYPE_NAMES fallback map');
|
||||
});
|
||||
|
||||
test('packets.js: hard guard for missing PayloadLabels', () => {
|
||||
// Must throw, not silently fall back.
|
||||
assert(/!\s*window\.PayloadLabels[\s\S]{0,80}throw/.test(packetsSrc),
|
||||
'packets.js missing `!window.PayloadLabels → throw` guard');
|
||||
});
|
||||
|
||||
test('packet-filter.js: _FALLBACK_FW + _FALLBACK_ALIASES gone', () => {
|
||||
assert(!/_FALLBACK_FW\s*=\s*\{/.test(filterSrc),
|
||||
'packet-filter.js still declares _FALLBACK_FW');
|
||||
assert(!/_FALLBACK_ALIASES\s*=\s*\{/.test(filterSrc),
|
||||
'packet-filter.js still declares _FALLBACK_ALIASES');
|
||||
});
|
||||
|
||||
test('packet-filter.js: hard guard for missing PayloadLabels', () => {
|
||||
assert(/!\s*window\.PayloadLabels[\s\S]{0,120}throw/.test(filterSrc),
|
||||
'packet-filter.js missing throw-guard');
|
||||
});
|
||||
|
||||
test('live.js buildLegendHtml: INLINE_LABELS fallback is gone', () => {
|
||||
assert(!/INLINE_LABELS\s*=\s*\{/.test(liveSrc),
|
||||
'live.js still declares INLINE_LABELS fallback inside buildLegendHtml');
|
||||
});
|
||||
|
||||
test('live.js: hard guard for missing PayloadLabels at top of file', () => {
|
||||
assert(/!\s*window\.PayloadLabels[\s\S]{0,120}throw/.test(liveSrc),
|
||||
'live.js missing throw-guard');
|
||||
});
|
||||
|
||||
console.log('=== ' + pass + ' passed, ' + fail + ' failed ===');
|
||||
process.exit(fail === 0 ? 0 : 1);
|
||||
Reference in New Issue
Block a user