Files
meshcore-analyzer/test-payload-labels-content.js
Kpa-clawbot b74a64ccfa fix(ui): canonical payload label map across packets/live/packet-filter (#1799) (#1804)
## 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>
2026-06-30 05:48:47 -07:00

92 lines
3.9 KiB
JavaScript

/* 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);